diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 4903ba5..c8bb929 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,10 +3,10 @@ "isRoot": true, "tools": { "cake.tool": { - "version": "1.3.0", + "version": "2.2.0", "commands": [ "dotnet-cake" ] } } -} \ No newline at end of file +} diff --git a/.editorconfig b/.editorconfig index 94ba4be..30b58f9 100644 --- a/.editorconfig +++ b/.editorconfig @@ -111,3 +111,7 @@ resharper_wrap_object_and_collection_initializer_style = wrap_if_long resharper_xmldoc_attribute_indent = align_by_first_attribute resharper_xmldoc_attribute_style = on_single_line resharper_xmldoc_pi_attribute_style = on_single_line + +[*.md] +trim_trailing_whitespace = false +indent_size = 2 diff --git a/.gitignore b/.gitignore index 78ff2f1..a87c755 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,4 @@ obj packages *.user tools -TestResults \ No newline at end of file +TestResults* diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a2cec2..325278e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,9 @@ Additions Improvements - Improved EnumHelper.GetValues signature to return an array - Allow using external gesture handling alongside InputHandler through ExternalGestureHandling +- Discard old data when updating a StaticSpriteBatch +- **Drastically improved StaticSpriteBatch batching performance** +- Multi-target net452, making MLEM compatible with MonoGame for consoles Fixes - Fixed TokenizedString handling trailing spaces incorrectly in the last line of non-left aligned text @@ -28,26 +31,45 @@ Fixes Additions - Added some extension methods for querying Anchor types - Added Element.AutoSizeAddedAbsolute to allow for more granular control of auto-sizing +- Added Element.OnAddedToUi and Element.OnRemovedFromUi +- Added ScrollBar.MouseDragScrolling +- Added Panel.ScrollToElement Improvements - Allow elements to auto-adjust their size even when their children are aligned oddly - Close other dropdowns when opening a dropdown - Generified UiMarkdownParser by adding abstract UiParser +- Multi-target net452, making MLEM compatible with MonoGame for consoles Fixes - Fixed parents of elements that prevent spill not being notified properly - Fixed paragraphs sometimes not updating their position properly when hidden because they're empty - Fixed panels sometimes not drawing children that came into view when their positions changed unexpectedly - Fixed UiMarkdownParser not parsing formatting in headings and blockquotes +- Fixed Element.OnChildAdded and Element.OnChildRemoved being called for grandchildren when a child is added +- Fixed an exception when trying to force-update the area of an element without a ui system +- Fixed the scroll bar of an empty panel being positioned incorrectly ### MLEM.Data +Additions +- Added data, from, and copy instructions to DataTextureAtlas + Improvements - Allow data texture atlas pivots and offsets to be negative - Made RuntimeTexturePacker restore texture region name and pivot when packing +- Multi-target net452, making MLEM compatible with MonoGame for consoles Fixes - Fixed data texture atlases not allowing most characters in their region names +## MLEM.Extended +Improvements +- Multi-target net452, making MLEM compatible with MonoGame for consoles + +## MLEM.Startup +Improvements +- Multi-target net452, making MLEM compatible with MonoGame for consoles + ## 6.0.0 ### MLEM diff --git a/Demos.Android/Demos.Android.csproj b/Demos.Android/Demos.Android.csproj index 771320e..87ec7ba 100644 --- a/Demos.Android/Demos.Android.csproj +++ b/Demos.Android/Demos.Android.csproj @@ -7,15 +7,16 @@ 1 1.0 true + false - + - + - + diff --git a/Demos.DesktopGL/Demos.DesktopGL.FNA.csproj b/Demos.DesktopGL/Demos.DesktopGL.FNA.csproj index 7933bf0..3835c88 100644 --- a/Demos.DesktopGL/Demos.DesktopGL.FNA.csproj +++ b/Demos.DesktopGL/Demos.DesktopGL.FNA.csproj @@ -7,6 +7,7 @@ MLEM Desktop Demos Demos.DesktopGL $(DefineConstants);FNA + false DesktopGL diff --git a/Demos.DesktopGL/Demos.DesktopGL.csproj b/Demos.DesktopGL/Demos.DesktopGL.csproj index 81b788e..d53a9bc 100644 --- a/Demos.DesktopGL/Demos.DesktopGL.csproj +++ b/Demos.DesktopGL/Demos.DesktopGL.csproj @@ -1,31 +1,32 @@  - + Exe net6.0 Icon.ico MLEM Desktop Demos + false - + - + - + - + - + diff --git a/Demos/Demos.FNA.csproj b/Demos/Demos.FNA.csproj index 6d088d2..a15422e 100644 --- a/Demos/Demos.FNA.csproj +++ b/Demos/Demos.FNA.csproj @@ -4,6 +4,7 @@ netstandard2.0 Demos $(DefineConstants);FNA + false diff --git a/Demos/Demos.csproj b/Demos/Demos.csproj index e0742c1..31946ae 100644 --- a/Demos/Demos.csproj +++ b/Demos/Demos.csproj @@ -2,6 +2,7 @@ netstandard2.0 + false diff --git a/Docs/index.md b/Docs/index.md index 01de279..05b2caa 100644 --- a/Docs/index.md +++ b/Docs/index.md @@ -2,6 +2,8 @@ **MLEM Library for Extending MonoGame and FNA** is a set of multipurpose libraries for the game frameworks [MonoGame](https://www.monogame.net/) and [FNA](https://fna-xna.github.io/) that provides abstractions, quality of life improvements and additional features like an extensive ui system and easy input handling. +MLEM is platform-agnostic and multi-targets .NET Standard 2.0, .NET 6.0 and .NET Framework 4.5.2, which makes it compatible with MonoGame and FNA on Desktop, mobile devices and consoles. + # What next? - Get it on [NuGet](https://www.nuget.org/packages?q=mlem) - Get prerelease builds on [BaGet](https://nuget.ellpeck.de/?q=mlem) diff --git a/FNA.Settings.props b/FNA.Settings.props new file mode 100644 index 0000000..0a0b3e8 --- /dev/null +++ b/FNA.Settings.props @@ -0,0 +1,15 @@ + + + + AnyCPU + false + + + + + + + + + + diff --git a/FontStashSharp b/FontStashSharp index 38849f3..6e6fc60 160000 --- a/FontStashSharp +++ b/FontStashSharp @@ -1 +1 @@ -Subproject commit 38849f3ac2887c14b8fa1c69c17468032e5233e1 +Subproject commit 6e6fc608bee0d4e7b2944f7c686ca0d28896e195 diff --git a/MLEM.Data/DataTextureAtlas.cs b/MLEM.Data/DataTextureAtlas.cs index 400a200..e163e72 100644 --- a/MLEM.Data/DataTextureAtlas.cs +++ b/MLEM.Data/DataTextureAtlas.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -5,10 +6,9 @@ using System.Text.RegularExpressions; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; -using MLEM.Textures; -#if FNA using MLEM.Extensions; -#endif +using MLEM.Misc; +using MLEM.Textures; namespace MLEM.Data { /// @@ -18,15 +18,20 @@ namespace MLEM.Data { /// /// /// Data texture atlases are designed to be easy to write by hand. Because of this, their structure is very simple. - /// Each texture region defined in the atlas consists of its name, followed by a set of possible keywords and their arguments, separated by spaces. - /// The loc keyword defines the of the texture region as a rectangle whose origin is its top-left corner. It requires four arguments: x, y, width and height of the rectangle. - /// The (optional) piv keyword defines the of the texture region. It requires two arguments: x and y. If it is not supplied, the pivot defaults to the top-left corner of the texture region. - /// The (optional) off keyword defines an offset that is added onto the location and pivot of this texture region. This is useful when copying and pasting a previously defined texture region to create a second region that has a constant offset. It requires two arguments: The x and y offset. + /// Each texture region defined in the atlas consists of its names (where multiple names can be separated by whitespace), followed by a set of possible instructions and their arguments, also separated by whitespace. + /// + /// The loc (or location) instruction defines the of the texture region as a rectangle whose origin is its top-left corner. It requires four arguments: x, y, width and height of the rectangle. + /// The (optional) piv (or pivot) instruction defines the of the texture region. It requires two arguments: x and y. If it is not supplied, the pivot defaults to the top-left corner of the texture region. + /// The (optional) off (of offset) instruction defines an offset that is added onto the location and pivot of this texture region. This is useful when duplicating a previously defined texture region to create a second region that has a constant offset. It requires two arguments: The x and y offset. + /// The (optional and repeatable) cpy (or copy) instruction defines an additional texture region that should also be generated from the same data, but with a given offset that will be applied to the location and pivot. It requires three arguments: the copy region's name and the x and y offsets. + /// The (optional and repeatable) dat (or data) instruction defines a custom data point that can be added to the resulting 's data. It requires two arguments: the data point's name and the data point's value, the latter of which is also stored as a string value. + /// The (optional) frm (or from) instruction defines a texture region (defined before the current region) whose data should be copied. All data from the region will be copied, but adding additional instructions afterwards modifies the data. It requires one argument: the name of the region whose data to copy. If this instruction is used, the loc instruction is not required. + /// /// /// - /// The following entry defines a texture region with the name LongTableRight, whose will be a rectangle with X=32, Y=30, Width=64, Height=48, and whose will be a vector with X=80, Y=46. + /// The following entry defines a texture region with the names LongTableRight and LongTableUp, whose will be a rectangle with X=32, Y=30, Width=64, Height=48, and whose will be a vector with X=80, Y=46. /// - /// LongTableRight + /// LongTableRight LongTableUp /// loc 32 30 64 48 /// piv 80 46 /// @@ -69,6 +74,7 @@ namespace MLEM.Data { /// /// Loads a from the given loaded texture and texture data file. + /// For more information on data texture atlases, see the type documentation. /// /// The texture to use for this data texture atlas /// The content manager to use for loading @@ -78,45 +84,124 @@ namespace MLEM.Data { public static DataTextureAtlas LoadAtlasData(TextureRegion texture, ContentManager content, string infoPath, bool pivotRelative = false) { var info = Path.Combine(content.RootDirectory, infoPath); string text; - if (Path.IsPathRooted(info)) { - text = File.ReadAllText(info); - } else { - using (var reader = new StreamReader(TitleContainer.OpenStream(info))) - text = reader.ReadToEnd(); + try { + if (Path.IsPathRooted(info)) { + text = File.ReadAllText(info); + } else { + using (var reader = new StreamReader(TitleContainer.OpenStream(info))) + text = reader.ReadToEnd(); + } + } catch (Exception e) { + throw new ContentLoadException($"Couldn't load data texture atlas data from {info}", e); } var atlas = new DataTextureAtlas(texture); + var words = Regex.Split(text, @"\s+"); - // parse each texture region: " loc [piv ] [off ]" - foreach (Match match in Regex.Matches(text, @"(.+)\s+loc\s+([0-9+]+)\s+([0-9+]+)\s+([0-9+]+)\s+([0-9+]+)\s*(?:piv\s+([0-9.+-]+)\s+([0-9.+-]+))?\s*(?:off\s+([0-9.+-]+)\s+([0-9.+-]+))?")) { - // offset - var off = !match.Groups[8].Success ? Vector2.Zero : new Vector2( - float.Parse(match.Groups[8].Value, CultureInfo.InvariantCulture), - float.Parse(match.Groups[9].Value, CultureInfo.InvariantCulture)); + var namesOffsets = new List<(string, Vector2)>(); + var customData = new Dictionary(); + var location = Rectangle.Empty; + var pivot = Vector2.Zero; + var offset = Vector2.Zero; + for (var i = 0; i < words.Length; i++) { + var word = words[i]; + try { + switch (word) { + case "loc": + case "location": + location = new Rectangle( + int.Parse(words[i + 1], CultureInfo.InvariantCulture), int.Parse(words[i + 2], CultureInfo.InvariantCulture), + int.Parse(words[i + 3], CultureInfo.InvariantCulture), int.Parse(words[i + 4], CultureInfo.InvariantCulture)); + i += 4; + break; + case "piv": + case "pivot": + pivot = new Vector2( + float.Parse(words[i + 1], CultureInfo.InvariantCulture), + float.Parse(words[i + 2], CultureInfo.InvariantCulture)); + i += 2; + break; + case "off": + case "offset": + offset = new Vector2( + float.Parse(words[i + 1], CultureInfo.InvariantCulture), + float.Parse(words[i + 2], CultureInfo.InvariantCulture)); + i += 2; + break; + case "cpy": + case "copy": + var copyOffset = new Vector2( + float.Parse(words[i + 2], CultureInfo.InvariantCulture), + float.Parse(words[i + 3], CultureInfo.InvariantCulture)); + namesOffsets.Add((words[i + 1], copyOffset)); + i += 3; + break; + case "dat": + case "data": + customData.Add(words[i + 1], words[i + 2]); + i += 2; + break; + case "frm": + case "from": + var fromRegion = atlas[words[i + 1]]; + customData.Clear(); + foreach (var key in fromRegion.GetDataKeys()) + customData.Add(key, fromRegion.GetData(key)); + // our main texture might be a sub-region already, so we have to take that into account + location = fromRegion.Area.OffsetCopy(new Point(-texture.U, -texture.V)); + pivot = fromRegion.PivotPixels; + if (pivot != Vector2.Zero && !pivotRelative) + pivot += location.Location.ToVector2(); + offset = Vector2.Zero; + i += 1; + break; + default: + // if we have data for the previous regions, they're valid so we add them + AddCurrentRegions(); - // location - var loc = new Rectangle( - int.Parse(match.Groups[2].Value), int.Parse(match.Groups[3].Value), - int.Parse(match.Groups[4].Value), int.Parse(match.Groups[5].Value)); - loc.Offset(off.ToPoint()); - - // pivot - var piv = !match.Groups[6].Success ? Vector2.Zero : off + new Vector2( - float.Parse(match.Groups[6].Value, CultureInfo.InvariantCulture) - (pivotRelative ? 0 : loc.X), - float.Parse(match.Groups[7].Value, CultureInfo.InvariantCulture) - (pivotRelative ? 0 : loc.Y)); - - foreach (var name in Regex.Split(match.Groups[1].Value, @"\s")) { - var trimmed = name.Trim(); - if (trimmed.Length <= 0) - continue; - var region = new TextureRegion(texture, loc) { - PivotPixels = piv, - Name = trimmed - }; - atlas.regions.Add(trimmed, region); + // we're starting a new region (or adding another name for a new region), so clear old data + namesOffsets.Add((word.Trim(), Vector2.Zero)); + customData.Clear(); + location = Rectangle.Empty; + pivot = Vector2.Zero; + offset = Vector2.Zero; + break; + } + } catch (Exception e) { + throw new ContentLoadException($"Couldn't parse data texture atlas instruction {word} for region(s) {string.Join(", ", namesOffsets)}", e); } } + // add the last region that was started on + AddCurrentRegions(); return atlas; + + void AddCurrentRegions() { + // the location is the only mandatory information, which is why we check it here + if (location == Rectangle.Empty || namesOffsets.Count <= 0) + return; + foreach (var (name, addedOff) in namesOffsets) { + var loc = location; + var piv = pivot; + var off = offset + addedOff; + + loc.Offset(off.ToPoint()); + if (piv != Vector2.Zero) { + piv += off; + if (!pivotRelative) + piv -= loc.Location.ToVector2(); + } + + var region = new TextureRegion(texture, loc) { + PivotPixels = piv, + Name = name + }; + foreach (var kv in customData) + region.SetData(kv.Key, kv.Value); + atlas.regions.Add(name, region); + } + // we only clear names offsets if the location was valid, otherwise we ignore multiple names for a region + namesOffsets.Clear(); + } } } @@ -128,6 +213,7 @@ namespace MLEM.Data { /// /// Loads a from the given texture and texture data file. + /// For more information on data texture atlases, see the type documentation. /// /// The content manager to use for loading /// The path to the texture file diff --git a/MLEM.Data/Json/JsonTypeSafeGenericDataHolder.cs b/MLEM.Data/Json/JsonTypeSafeGenericDataHolder.cs index d3cbbbe..39f2d1c 100644 --- a/MLEM.Data/Json/JsonTypeSafeGenericDataHolder.cs +++ b/MLEM.Data/Json/JsonTypeSafeGenericDataHolder.cs @@ -12,6 +12,8 @@ namespace MLEM.Data.Json { [DataContract] public class JsonTypeSafeGenericDataHolder : IGenericDataHolder { + private static readonly string[] EmptyStrings = new string[0]; + [DataMember(EmitDefaultValue = false)] private Dictionary data; @@ -35,9 +37,9 @@ namespace MLEM.Data.Json { } /// - public IReadOnlyCollection GetDataKeys() { + public IEnumerable GetDataKeys() { if (this.data == null) - return Array.Empty(); + return JsonTypeSafeGenericDataHolder.EmptyStrings; return this.data.Keys; } diff --git a/MLEM.Data/MLEM.Data.FNA.csproj b/MLEM.Data/MLEM.Data.FNA.csproj index d4dae9c..4c9ffa5 100644 --- a/MLEM.Data/MLEM.Data.FNA.csproj +++ b/MLEM.Data/MLEM.Data.FNA.csproj @@ -1,6 +1,6 @@  - netstandard2.0 + net452;netstandard2.0;net6.0 true true MLEM.Data @@ -33,7 +33,8 @@ all - + + all diff --git a/MLEM.Data/MLEM.Data.csproj b/MLEM.Data/MLEM.Data.csproj index 4d47947..12fe703 100644 --- a/MLEM.Data/MLEM.Data.csproj +++ b/MLEM.Data/MLEM.Data.csproj @@ -1,6 +1,6 @@  - netstandard2.0 + net452;netstandard2.0;net6.0 true true NU1701 diff --git a/MLEM.Extended/MLEM.Extended.FNA.csproj b/MLEM.Extended/MLEM.Extended.FNA.csproj index dd8d810..b671c22 100644 --- a/MLEM.Extended/MLEM.Extended.FNA.csproj +++ b/MLEM.Extended/MLEM.Extended.FNA.csproj @@ -1,10 +1,11 @@  - netstandard2.0 + netstandard2.0;net6.0 true true MLEM.Extended $(DefineConstants);FNA + NU1702 @@ -22,10 +23,10 @@ - + all - + all diff --git a/MLEM.Extended/MLEM.Extended.csproj b/MLEM.Extended/MLEM.Extended.csproj index f66d8fb..ff63ed3 100644 --- a/MLEM.Extended/MLEM.Extended.csproj +++ b/MLEM.Extended/MLEM.Extended.csproj @@ -1,10 +1,10 @@  - netstandard2.0 + netstandard2.0;net6.0 true true - + Ellpeck MLEM Library for Extending MonoGame extension that ties in with MonoGame.Extended and other MonoGame libraries @@ -16,10 +16,10 @@ Logo.png README.md - + - + all @@ -33,9 +33,9 @@ all - + - \ No newline at end of file + diff --git a/MLEM.FNA.sln b/MLEM.FNA.sln index acdea53..d4e2539 100644 --- a/MLEM.FNA.sln +++ b/MLEM.FNA.sln @@ -16,9 +16,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.FNA", "Tests\Tests.FN EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MLEM.Extended.FNA", "MLEM.Extended\MLEM.Extended.FNA.csproj", "{A5B22930-DF4B-4A62-93ED-A6549F7B666B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FNA.Core", "FNA\FNA.Core.csproj", "{06459F72-CEAA-4B45-B2B1-708FC28D04F8}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FNA", "FNA\FNA.csproj", "{35253CE1-C864-4CD3-8249-4D1319748E8F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FontStashSharp.FNA.Core", "FontStashSharp\src\XNA\FontStashSharp.FNA.Core.csproj", "{0B410591-3AED-4C82-A07A-516FF493709B}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FontStashSharp.FNA", "FontStashSharp\src\XNA\FontStashSharp.FNA.csproj", "{39249E92-EBF2-4951-A086-AB4951C3CCE1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FNA.Core", "FNA\FNA.Core.csproj", "{458FFA5E-A1C4-4B23-A5D8-259385FEECED}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -58,13 +60,17 @@ Global {A5B22930-DF4B-4A62-93ED-A6549F7B666B}.Debug|Any CPU.Build.0 = Debug|Any CPU {A5B22930-DF4B-4A62-93ED-A6549F7B666B}.Release|Any CPU.ActiveCfg = Release|Any CPU {A5B22930-DF4B-4A62-93ED-A6549F7B666B}.Release|Any CPU.Build.0 = Release|Any CPU - {06459F72-CEAA-4B45-B2B1-708FC28D04F8}.Debug|Any CPU.ActiveCfg = Debug|x64 - {06459F72-CEAA-4B45-B2B1-708FC28D04F8}.Debug|Any CPU.Build.0 = Debug|x64 - {06459F72-CEAA-4B45-B2B1-708FC28D04F8}.Release|Any CPU.ActiveCfg = Release|x64 - {06459F72-CEAA-4B45-B2B1-708FC28D04F8}.Release|Any CPU.Build.0 = Release|x64 - {0B410591-3AED-4C82-A07A-516FF493709B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0B410591-3AED-4C82-A07A-516FF493709B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0B410591-3AED-4C82-A07A-516FF493709B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0B410591-3AED-4C82-A07A-516FF493709B}.Release|Any CPU.Build.0 = Release|Any CPU + {35253CE1-C864-4CD3-8249-4D1319748E8F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {35253CE1-C864-4CD3-8249-4D1319748E8F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {35253CE1-C864-4CD3-8249-4D1319748E8F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {35253CE1-C864-4CD3-8249-4D1319748E8F}.Release|Any CPU.Build.0 = Release|Any CPU + {39249E92-EBF2-4951-A086-AB4951C3CCE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {39249E92-EBF2-4951-A086-AB4951C3CCE1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {39249E92-EBF2-4951-A086-AB4951C3CCE1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {39249E92-EBF2-4951-A086-AB4951C3CCE1}.Release|Any CPU.Build.0 = Release|Any CPU + {458FFA5E-A1C4-4B23-A5D8-259385FEECED}.Debug|Any CPU.ActiveCfg = Debug|x64 + {458FFA5E-A1C4-4B23-A5D8-259385FEECED}.Debug|Any CPU.Build.0 = Debug|x64 + {458FFA5E-A1C4-4B23-A5D8-259385FEECED}.Release|Any CPU.ActiveCfg = Release|x64 + {458FFA5E-A1C4-4B23-A5D8-259385FEECED}.Release|Any CPU.Build.0 = Release|x64 EndGlobalSection EndGlobal diff --git a/MLEM.Startup/MLEM.Startup.FNA.csproj b/MLEM.Startup/MLEM.Startup.FNA.csproj index 57e5474..b5f3a64 100644 --- a/MLEM.Startup/MLEM.Startup.FNA.csproj +++ b/MLEM.Startup/MLEM.Startup.FNA.csproj @@ -1,7 +1,7 @@  - netstandard2.0 + net452;netstandard2.0;net6.0 true true MLEM.Startup @@ -21,11 +21,11 @@ - + - + all diff --git a/MLEM.Startup/MLEM.Startup.csproj b/MLEM.Startup/MLEM.Startup.csproj index 95db618..f7f863d 100644 --- a/MLEM.Startup/MLEM.Startup.csproj +++ b/MLEM.Startup/MLEM.Startup.csproj @@ -1,11 +1,11 @@  - + - netstandard2.0 + net452;netstandard2.0;net6.0 true true - + Ellpeck MLEM Library for Extending MonoGame combined with some other useful libraries into a quick Game startup class @@ -17,17 +17,17 @@ Logo.png README.md - + - + - + all - + diff --git a/MLEM.Templates/MLEM.Templates.csproj b/MLEM.Templates/MLEM.Templates.csproj index ac2d3e9..f321339 100644 --- a/MLEM.Templates/MLEM.Templates.csproj +++ b/MLEM.Templates/MLEM.Templates.csproj @@ -1,14 +1,14 @@  - + - netstandard2.0 + net452;netstandard2.0;net6.0 true false content true NU5128 - + Template MLEM Templates @@ -21,7 +21,7 @@ Logo.png README.md - + @@ -29,4 +29,4 @@ - \ No newline at end of file + diff --git a/MLEM.Ui/Elements/Element.cs b/MLEM.Ui/Elements/Element.cs index ccf3748..049ed6e 100644 --- a/MLEM.Ui/Elements/Element.cs +++ b/MLEM.Ui/Elements/Element.cs @@ -408,13 +408,23 @@ namespace MLEM.Ui.Elements { public GamepadNextElementCallback GetGamepadNextElement; /// /// Event that is called when a child is added to this element using + /// Note that, while this event is only called for immediate children of this element, is called for all children and grandchildren. /// public OtherElementCallback OnChildAdded; /// - /// Event that is called when a child is removed from this element using + /// Event that is called when a child is removed from this element using . + /// Note that, while this event is only called for immediate children of this element, is called for all children and grandchildren. /// public OtherElementCallback OnChildRemoved; /// + /// Event that is called when this element is added to a , that is, when this element's is set to a non- value. + /// + public GenericCallback OnAddedToUi; + /// + /// Event that is called when this element is removed from a , that is, when this element's is set to . + /// + public GenericCallback OnRemovedFromUi; + /// /// Event that is called when this element's method is called, which also happens in . /// This event is useful for unregistering global event handlers when this object should be destroyed. /// @@ -442,7 +452,7 @@ namespace MLEM.Ui.Elements { /// The of this element's , or the if this element has no parent. /// This value is the one that is passed to during . /// - protected RectangleF ParentArea => this.Parent?.ChildPaddedArea ?? (RectangleF) this.system.Viewport; + protected RectangleF ParentArea => this.Parent?.ChildPaddedArea ?? (RectangleF) this.System.Viewport; private readonly List children = new List(); private readonly Stopwatch stopwatch = new Stopwatch(); @@ -497,9 +507,10 @@ namespace MLEM.Ui.Elements { element.AndChildren(e => { e.Root = this.Root; e.System = this.System; + e.OnAddedToUi?.Invoke(e); this.Root?.InvokeOnElementAdded(e); - this.OnChildAdded?.Invoke(this, e); }); + this.OnChildAdded?.Invoke(this, element); this.SetSortedChildrenDirty(); element.SetAreaDirty(); return element; @@ -520,9 +531,10 @@ namespace MLEM.Ui.Elements { element.AndChildren(e => { e.Root = null; e.System = null; + e.OnRemovedFromUi?.Invoke(e); this.Root?.InvokeOnElementRemoved(e); - this.OnChildRemoved?.Invoke(this, e); }); + this.OnChildRemoved?.Invoke(this, element); this.SetSortedChildrenDirty(); } @@ -589,7 +601,7 @@ namespace MLEM.Ui.Elements { /// public virtual void ForceUpdateArea() { this.AreaDirty = false; - if (this.IsHidden) + if (this.IsHidden || this.System == null) return; // if the parent's area is dirty, it would get updated anyway when querying its ChildPaddedArea, // which would cause our ForceUpdateArea code to be run twice, so we only update our parent instead diff --git a/MLEM.Ui/Elements/Panel.cs b/MLEM.Ui/Elements/Panel.cs index 4be979a..dafda63 100644 --- a/MLEM.Ui/Elements/Panel.cs +++ b/MLEM.Ui/Elements/Panel.cs @@ -84,8 +84,7 @@ namespace MLEM.Ui.Elements { return; if (e == null || !e.GetParentTree().Contains(this)) return; - var firstChild = this.Children.First(c => c != this.ScrollBar); - this.ScrollBar.CurrentValue = (e.Area.Center.Y - this.Area.Height / 2 - firstChild.Area.Top) / e.Scale + this.ChildPadding.Value.Height / 2; + this.ScrollToElement(e); }; this.AddChild(this.ScrollBar); } @@ -115,15 +114,6 @@ namespace MLEM.Ui.Elements { this.ScrollSetup(); } - private void ScrollChildren() { - if (!this.scrollOverflow) - return; - // we ignore false grandchildren so that the children of the scroll bar stay in place - foreach (var child in this.GetChildren(c => c != this.ScrollBar, true, true)) - child.ScrollOffset.Y = -this.ScrollBar.CurrentValue; - this.relevantChildrenDirty = true; - } - /// public override void ForceUpdateSortedChildren() { base.ForceUpdateSortedChildren(); @@ -143,26 +133,6 @@ namespace MLEM.Ui.Elements { base.RemoveChildren(e => e != this.ScrollBar && (condition == null || condition(e))); } - /// - protected override IList GetRelevantChildren() { - var relevant = base.GetRelevantChildren(); - if (this.scrollOverflow) { - if (this.relevantChildrenDirty) - this.ForceUpdateRelevantChildren(); - relevant = this.relevantChildren; - } - return relevant; - } - - /// - protected override void OnChildAreaDirty(Element child, bool grandchild) { - base.OnChildAreaDirty(child, grandchild); - // we only need to scroll when a grandchild changes, since all of our children are forced - // to be auto-anchored and so will automatically propagate their changes up to us - if (grandchild) - this.ScrollChildren(); - } - /// public override void Draw(GameTime time, SpriteBatch batch, float alpha, SpriteBatchContext context) { // draw children onto the render target if we have one @@ -207,11 +177,32 @@ namespace MLEM.Ui.Elements { return base.GetElementUnderPos(position); } - private RectangleF GetRenderTargetArea() { - var area = this.ChildPaddedArea; - area.X = this.DisplayArea.X; - area.Width = this.DisplayArea.Width; - return area; + /// + public override void Dispose() { + if (this.renderTarget != null) { + this.renderTarget.Dispose(); + this.renderTarget = null; + } + base.Dispose(); + } + + /// + /// Scrolls this panel's to the given in such a way that its center is positioned in the center of this panel. + /// + /// The element to scroll to. + public void ScrollToElement(Element element) { + this.ScrollToElement(element.Area.Center.Y); + } + + /// + /// Scrolls this panel's to the given coordinate in such a way that the coordinate is positioned in the center of this panel. + /// + /// The y coordinate to scroll to, which should have this element's applied. + public void ScrollToElement(float elementY) { + var firstChild = this.Children.FirstOrDefault(c => c != this.ScrollBar); + if (firstChild == null) + return; + this.ScrollBar.CurrentValue = (elementY - this.Area.Height / 2 - firstChild.Area.Top) / this.Scale + this.ChildPadding.Value.Height / 2; } /// @@ -225,34 +216,56 @@ namespace MLEM.Ui.Elements { this.SetScrollBarStyle(); } + /// + protected override IList GetRelevantChildren() { + var relevant = base.GetRelevantChildren(); + if (this.scrollOverflow) { + if (this.relevantChildrenDirty) + this.ForceUpdateRelevantChildren(); + relevant = this.relevantChildren; + } + return relevant; + } + + /// + protected override void OnChildAreaDirty(Element child, bool grandchild) { + base.OnChildAreaDirty(child, grandchild); + // we only need to scroll when a grandchild changes, since all of our children are forced + // to be auto-anchored and so will automatically propagate their changes up to us + if (grandchild) + this.ScrollChildren(); + } + /// /// Prepares the panel for auto-scrolling, creating the render target and setting up the scroll bar's maximum value. /// protected virtual void ScrollSetup() { if (!this.scrollOverflow || this.IsHidden) return; - // if there is only one child, then we have just the scroll bar - if (this.Children.Count == 1) - return; - // the "real" first child is the scroll bar, which we want to ignore - var firstChild = this.Children.First(c => c != this.ScrollBar); - var lowestChild = this.GetLowestChild(c => c != this.ScrollBar && !c.IsHidden); - var childrenHeight = lowestChild.Area.Bottom - firstChild.Area.Top; + float childrenHeight; + if (this.Children.Count > 1) { + var firstChild = this.Children.FirstOrDefault(c => c != this.ScrollBar); + var lowestChild = this.GetLowestChild(c => c != this.ScrollBar && !c.IsHidden); + childrenHeight = lowestChild.Area.Bottom - firstChild.Area.Top; + } else { + // if we only have one child (the scroll bar), then the children take up no visual height + childrenHeight = 0; + } - // the max value of the scrollbar is the amount of non-scaled pixels taken up by overflowing components + // the max value of the scroll bar is the amount of non-scaled pixels taken up by overflowing components var scrollBarMax = (childrenHeight - this.ChildPaddedArea.Height) / this.Scale; if (!this.ScrollBar.MaxValue.Equals(scrollBarMax, Element.Epsilon)) { this.ScrollBar.MaxValue = scrollBarMax; this.relevantChildrenDirty = true; + } - // update child padding based on whether the scroll bar is visible - var childOffset = this.ScrollBar.IsHidden ? 0 : this.ScrollerSize.Value.X + this.ScrollBarOffset; - if (!this.scrollBarChildOffset.Equals(childOffset, Element.Epsilon)) { - this.ChildPadding += new Padding(0, -this.scrollBarChildOffset + childOffset, 0, 0); - this.scrollBarChildOffset = childOffset; - this.SetAreaDirty(); - } + // update child padding based on whether the scroll bar is visible + var childOffset = this.ScrollBar.IsHidden ? 0 : this.ScrollerSize.Value.X + this.ScrollBarOffset; + if (!this.scrollBarChildOffset.Equals(childOffset, Element.Epsilon)) { + this.ChildPadding += new Padding(0, -this.scrollBarChildOffset + childOffset, 0, 0); + this.scrollBarChildOffset = childOffset; + this.SetAreaDirty(); } // the scroller height has the same relation to the scroll bar height as the visible area has to the total height of the panel's content @@ -271,15 +284,6 @@ namespace MLEM.Ui.Elements { } } - /// - public override void Dispose() { - if (this.renderTarget != null) { - this.renderTarget.Dispose(); - this.renderTarget = null; - } - base.Dispose(); - } - private void SetScrollBarStyle() { if (this.ScrollBar == null) return; @@ -306,5 +310,21 @@ namespace MLEM.Ui.Elements { } } + private RectangleF GetRenderTargetArea() { + var area = this.ChildPaddedArea; + area.X = this.DisplayArea.X; + area.Width = this.DisplayArea.Width; + return area; + } + + private void ScrollChildren() { + if (!this.scrollOverflow) + return; + // we ignore false grandchildren so that the children of the scroll bar stay in place + foreach (var child in this.GetChildren(c => c != this.ScrollBar, true, true)) + child.ScrollOffset.Y = -this.ScrollBar.CurrentValue; + this.relevantChildrenDirty = true; + } + } } diff --git a/MLEM.Ui/Elements/ScrollBar.cs b/MLEM.Ui/Elements/ScrollBar.cs index 541c282..e0fef5c 100644 --- a/MLEM.Ui/Elements/ScrollBar.cs +++ b/MLEM.Ui/Elements/ScrollBar.cs @@ -33,6 +33,16 @@ namespace MLEM.Ui.Elements { /// The texture of this scroll bar's scroller indicator /// public StyleProp ScrollerTexture; + /// + /// Whether smooth scrolling should be enabled for this scroll bar. + /// Smooth scrolling causes the to change gradually rather than instantly when scrolling. + /// + public StyleProp SmoothScrolling; + /// + /// The factor with which happens. + /// + public StyleProp SmoothScrollFactor; + /// /// The scroller's width and height /// @@ -86,7 +96,7 @@ namespace MLEM.Ui.Elements { /// /// This property is true while the user scrolls on the scroll bar using the mouse or touch input /// - public bool IsBeingScrolled => this.isMouseHeld || this.isDragging || this.isTouchHeld; + public bool IsBeingScrolled => this.isMouseScrolling || this.isMouseDragging || this.isTouchDragging || this.isTouchScrolling; /// /// This field determines if this scroll bar should automatically be hidden from a if there aren't enough children to allow for scrolling. /// @@ -99,18 +109,14 @@ namespace MLEM.Ui.Elements { !this.Horizontal ? 0 : this.CurrentValue / this.maxValue * (this.DisplayArea.Width - this.ScrollerSize.X * this.Scale), this.Horizontal ? 0 : this.CurrentValue / this.maxValue * (this.DisplayArea.Height - this.ScrollerSize.Y * this.Scale)); /// - /// Whether smooth scrolling should be enabled for this scroll bar. - /// Smooth scrolling causes the to change gradually rather than instantly when scrolling. + /// Whether this scroll bar should allow dragging the mouse over its attached 's content while holding the left mouse button to scroll, similarly to how scrolling using touch input works. /// - public StyleProp SmoothScrolling; - /// - /// The factor with which happens. - /// - public StyleProp SmoothScrollFactor; + public bool MouseDragScrolling; - private bool isMouseHeld; - private bool isDragging; - private bool isTouchHeld; + private bool isMouseScrolling; + private bool isMouseDragging; + private bool isTouchScrolling; + private bool isTouchDragging; private float maxValue; private float scrollAdded; private float currValue; @@ -141,18 +147,29 @@ namespace MLEM.Ui.Elements { // MOUSE INPUT var moused = this.Controls.MousedElement; - if (moused == this && this.Input.WasMouseButtonUp(MouseButton.Left) && this.Input.IsMouseButtonDown(MouseButton.Left)) { - this.isMouseHeld = true; + var wasMouseUp = this.Input.WasMouseButtonUp(MouseButton.Left); + var isMouseDown = this.Input.IsMouseButtonDown(MouseButton.Left); + if (moused == this && wasMouseUp && isMouseDown) { + this.isMouseScrolling = true; this.scrollStartOffset = this.TransformInverseAll(this.Input.ViewportMousePosition.ToVector2()) - this.ScrollerPosition; - } else if (this.isMouseHeld && !this.Input.IsMouseButtonDown(MouseButton.Left)) { - this.isMouseHeld = false; + } else if (!isMouseDown) { + this.isMouseScrolling = false; } - if (this.isMouseHeld) + if (this.isMouseScrolling) this.ScrollToPos(this.TransformInverseAll(this.Input.ViewportMousePosition.ToVector2())); - if (!this.Horizontal && moused != null && (moused == this.Parent || moused.GetParentTree().Contains(this.Parent))) { - var scroll = this.Input.LastScrollWheel - this.Input.ScrollWheel; - if (scroll != 0) - this.CurrentValue += this.StepPerScroll * Math.Sign(scroll); + if (!this.Horizontal) { + if (moused != null && (moused == this.Parent || moused.GetParentTree().Contains(this.Parent))) { + var scroll = this.Input.LastScrollWheel - this.Input.ScrollWheel; + if (scroll != 0) + this.CurrentValue += this.StepPerScroll * Math.Sign(scroll); + + if (this.MouseDragScrolling && moused != this && wasMouseUp && isMouseDown) + this.isMouseDragging = true; + } + if (!isMouseDown) + this.isMouseDragging = false; + if (this.isMouseDragging) + this.CurrentValue -= (this.Input.MousePosition.Y - this.Input.LastMousePosition.Y) / this.Scale; } // TOUCH INPUT @@ -162,29 +179,29 @@ namespace MLEM.Ui.Elements { // if the element under the drag's start position is on top of the panel, start dragging var touched = this.Parent.GetElementUnderPos(this.TransformInverseAll(drag.Position)); if (touched != null && touched != this) - this.isDragging = true; + this.isTouchDragging = true; // if we're dragging at all, then move the scroller - if (this.isDragging) + if (this.isTouchDragging) this.CurrentValue -= drag.Delta.Y / this.Scale; } else { - this.isDragging = false; + this.isTouchDragging = false; } } if (this.Input.ViewportTouchState.Count <= 0) { // if no touch has occured this tick, then reset the variable - this.isTouchHeld = false; + this.isTouchScrolling = false; } else { foreach (var loc in this.Input.ViewportTouchState) { var pos = this.TransformInverseAll(loc.Position); // if we just started touching and are on top of the scroller, then we should start scrolling if (this.DisplayArea.Contains(pos) && !loc.TryGetPreviousLocation(out _)) { - this.isTouchHeld = true; + this.isTouchScrolling = true; this.scrollStartOffset = pos - this.ScrollerPosition; break; } // scroll no matter if we're on the scroller right now - if (this.isTouchHeld) + if (this.isTouchScrolling) this.ScrollToPos(pos); } } diff --git a/MLEM.Ui/Elements/TextField.cs b/MLEM.Ui/Elements/TextField.cs index 9eafbd8..7b39c6f 100644 --- a/MLEM.Ui/Elements/TextField.cs +++ b/MLEM.Ui/Elements/TextField.cs @@ -6,7 +6,9 @@ using MLEM.Input; using MLEM.Misc; using MLEM.Textures; using MLEM.Ui.Style; +#if NETSTANDARD2_0_OR_GREATER || NET6_0_OR_GREATER using TextCopy; +#endif namespace MLEM.Ui.Elements { /// @@ -143,7 +145,11 @@ namespace MLEM.Ui.Elements { /// The text that the text field should contain by default /// Whether the text field should support multi-line editing public TextField(Anchor anchor, Vector2 size, Rule rule = null, GenericFont font = null, string text = null, bool multiline = false) : base(anchor, size) { - this.textInput = new TextInput(null, Vector2.Zero, 1, null, ClipboardService.SetText, ClipboardService.GetText) { + this.textInput = new TextInput(null, Vector2.Zero, 1 + #if NETSTANDARD2_0_OR_GREATER || NET6_0_OR_GREATER + , null, ClipboardService.SetText, ClipboardService.GetText + #endif + ) { OnTextChange = (i, s) => this.OnTextChange?.Invoke(this, s), InputRule = (i, s) => this.InputRule.Invoke(this, s) }; diff --git a/MLEM.Ui/MLEM.Ui.FNA.csproj b/MLEM.Ui/MLEM.Ui.FNA.csproj index 23f0414..3482793 100644 --- a/MLEM.Ui/MLEM.Ui.FNA.csproj +++ b/MLEM.Ui/MLEM.Ui.FNA.csproj @@ -1,6 +1,6 @@  - netstandard2.0 + net452;netstandard2.0;net6.0 true true MLEM.Ui @@ -20,10 +20,10 @@ - + - + all diff --git a/MLEM.Ui/MLEM.Ui.csproj b/MLEM.Ui/MLEM.Ui.csproj index e9aecaa..301173c 100644 --- a/MLEM.Ui/MLEM.Ui.csproj +++ b/MLEM.Ui/MLEM.Ui.csproj @@ -1,10 +1,10 @@  - netstandard2.0 + net452;netstandard2.0;net6.0 true true - + Ellpeck A mouse, keyboard, gamepad and touch ready Ui system for MonoGame that features automatic anchoring, sizing and several ready-to-use element types @@ -16,18 +16,18 @@ Logo.png README.md - + - + - + all - + - \ No newline at end of file + diff --git a/MLEM.Ui/Parsers/UiParser.cs b/MLEM.Ui/Parsers/UiParser.cs index 570d8e6..f7024fb 100644 --- a/MLEM.Ui/Parsers/UiParser.cs +++ b/MLEM.Ui/Parsers/UiParser.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Net.Http; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using MLEM.Formatting; @@ -10,6 +9,12 @@ using MLEM.Textures; using MLEM.Ui.Elements; using MLEM.Ui.Style; +#if NETSTANDARD2_0_OR_GREATER || NET6_0_OR_GREATER +using System.Net.Http; +#else +using System.Net; +#endif + namespace MLEM.Ui.Parsers { /// /// A base class for parsing various types of formatted strings into a set of MLEM.Ui elements with styling for each individual . @@ -148,15 +153,16 @@ namespace MLEM.Ui.Parsers { try { Texture2D tex; if (path.StartsWith("http")) { - using (var client = new HttpClient()) { - using (var src = await client.GetStreamAsync(path)) { - using (var memory = new MemoryStream()) { - // download the full stream before passing it to texture - await src.CopyToAsync(memory); - tex = Texture2D.FromStream(this.GraphicsDevice, memory); - } - } - } + byte[] src; + #if NETSTANDARD2_0_OR_GREATER || NET6_0_OR_GREATER + using (var client = new HttpClient()) + src = await client.GetByteArrayAsync(path); + #else + using (var client = new WebClient()) + src = await client.DownloadDataTaskAsync(path); + #endif + using (var memory = new MemoryStream(src)) + tex = Texture2D.FromStream(this.GraphicsDevice, memory); } else { using (var stream = Path.IsPathRooted(path) ? File.OpenRead(path) : TitleContainer.OpenStream(path)) tex = Texture2D.FromStream(this.GraphicsDevice, stream); diff --git a/MLEM.Ui/UiControls.cs b/MLEM.Ui/UiControls.cs index 8f47cf4..d02a413 100644 --- a/MLEM.Ui/UiControls.cs +++ b/MLEM.Ui/UiControls.cs @@ -9,6 +9,10 @@ using MLEM.Misc; using MLEM.Ui.Elements; using MLEM.Ui.Style; +#if NET452 +using MLEM.Extensions; +#endif + namespace MLEM.Ui { /// /// UiControls holds and manages all of the controls for a . diff --git a/MLEM.Ui/UiSystem.cs b/MLEM.Ui/UiSystem.cs index 2f4fd32..1e4587b 100644 --- a/MLEM.Ui/UiSystem.cs +++ b/MLEM.Ui/UiSystem.cs @@ -347,6 +347,7 @@ namespace MLEM.Ui { root.Element.AndChildren(e => { e.Root = root; e.System = this; + e.OnAddedToUi?.Invoke(e); root.InvokeOnElementAdded(e); e.SetAreaDirty(); }); @@ -369,6 +370,7 @@ namespace MLEM.Ui { root.Element.AndChildren(e => { e.Root = null; e.System = null; + e.OnRemovedFromUi?.Invoke(e); root.InvokeOnElementRemoved(e); e.SetAreaDirty(); }); @@ -570,7 +572,7 @@ namespace MLEM.Ui { /// public event Element.GenericCallback OnElementAdded; /// - /// Event that is invoked when a is removed rom this root element of any of its children. + /// Event that is invoked when a is removed rom this root element or any of its children. /// public event Element.GenericCallback OnElementRemoved; /// diff --git a/MLEM.sln b/MLEM.sln index 40fe144..13e208e 100644 --- a/MLEM.sln +++ b/MLEM.sln @@ -18,8 +18,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MLEM.Data", "MLEM.Data\MLEM EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MLEM.Templates", "MLEM.Templates\MLEM.Templates.csproj", "{C2A2CFED-C9E8-4675-BD66-EFC3DB210977}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Demos.Android", "Demos.Android\Demos.Android.csproj", "{410C0262-131C-4D0E-910D-D01B4F7143E0}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{53D52C3F-67FB-4F32-A794-EAB140BBFC11}" EndProject Global @@ -64,10 +62,6 @@ Global {C2A2CFED-C9E8-4675-BD66-EFC3DB210977}.Debug|Any CPU.Build.0 = Debug|Any CPU {C2A2CFED-C9E8-4675-BD66-EFC3DB210977}.Release|Any CPU.ActiveCfg = Release|Any CPU {C2A2CFED-C9E8-4675-BD66-EFC3DB210977}.Release|Any CPU.Build.0 = Release|Any CPU - {410C0262-131C-4D0E-910D-D01B4F7143E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {410C0262-131C-4D0E-910D-D01B4F7143E0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {410C0262-131C-4D0E-910D-D01B4F7143E0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {410C0262-131C-4D0E-910D-D01B4F7143E0}.Release|Any CPU.Build.0 = Release|Any CPU {53D52C3F-67FB-4F32-A794-EAB140BBFC11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {53D52C3F-67FB-4F32-A794-EAB140BBFC11}.Debug|Any CPU.Build.0 = Debug|Any CPU {53D52C3F-67FB-4F32-A794-EAB140BBFC11}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/MLEM/Extensions/CollectionExtensions.cs b/MLEM/Extensions/CollectionExtensions.cs index db1f238..f983751 100644 --- a/MLEM/Extensions/CollectionExtensions.cs +++ b/MLEM/Extensions/CollectionExtensions.cs @@ -51,5 +51,29 @@ namespace MLEM.Extensions { return combos; } + #if NET452 + /// Appends a value to the end of the sequence. + /// A sequence of values. + /// The value to append to . + /// The type of the elements of . + /// A new sequence that ends with . + public static IEnumerable Append(this IEnumerable source, T element) { + foreach (var src in source) + yield return src; + yield return element; + } + + /// Prepends a value to the beginning of the sequence. + /// A sequence of values. + /// The value to prepend to . + /// The type of the elements of . + /// A new sequence that begins with . + public static IEnumerable Prepend(this IEnumerable source, T element) { + yield return element; + foreach (var src in source) + yield return src; + } + #endif + } } diff --git a/MLEM/Graphics/StaticSpriteBatch.cs b/MLEM/Graphics/StaticSpriteBatch.cs index e9cf4cf..a707db7 100644 --- a/MLEM/Graphics/StaticSpriteBatch.cs +++ b/MLEM/Graphics/StaticSpriteBatch.cs @@ -10,7 +10,7 @@ using System.IO; namespace MLEM.Graphics { /// - /// A static sprite batch is a variation of that keeps all batched items in a , allowing for them to be drawn multiple times. + /// A static sprite batch is a highly optimized variation of that keeps all batched items in a , allowing for them to be drawn multiple times. /// To add items to a static sprite batch, use to begin batching, to clear currently batched items, Add and its various overloads to add batch items, to remove them again, and to end batching. /// To draw the batched items, call . /// @@ -23,7 +23,7 @@ namespace MLEM.Graphics { /// /// The amount of vertices that are currently batched. /// - public int Vertices => this.items.Count * 4; + public int Vertices => this.itemAmount * 4; /// /// The amount of vertex buffers that this static sprite batch has. /// To see the amount of buffers that are actually in use, see . @@ -41,13 +41,15 @@ namespace MLEM.Graphics { private readonly GraphicsDevice graphicsDevice; private readonly SpriteEffect spriteEffect; - - private readonly List vertexBuffers = new List(); + private readonly List vertexBuffers = new List(); private readonly List textures = new List(); - private readonly ISet items = new HashSet(); + private readonly SortedDictionary items = new SortedDictionary(); + + private SpriteSortMode sortMode = SpriteSortMode.Texture; private IndexBuffer indices; private bool batching; private bool batchChanged; + private int itemAmount; /// /// Creates a new static sprite batch with the given @@ -62,10 +64,27 @@ namespace MLEM.Graphics { /// Begins batching. /// Call this method before calling Add or any of its overloads. /// + /// The drawing order for sprite drawing. When is passed, the last used sort mode will be used again. The initial sort mode is . Note that is not supported. /// Thrown if this batch is currently batching already - public void BeginBatch() { + /// Thrown if the is , which is not supported. + public void BeginBatch(SpriteSortMode? sortMode = null) { if (this.batching) throw new InvalidOperationException("Already batching"); + if (sortMode == SpriteSortMode.Immediate) + throw new ArgumentOutOfRangeException(nameof(sortMode), "Cannot use sprite sort mode Immediate for static batching"); + + // if the sort mode changed (which should be very rare in practice), we have to re-sort our list + if (sortMode != null && this.sortMode != sortMode) { + this.sortMode = sortMode.Value; + if (this.items.Count > 0) { + var tempItems = this.items.Values.SelectMany(s => s.Items).ToArray(); + this.items.Clear(); + foreach (var item in tempItems) + this.AddItemToSet(item); + this.batchChanged = true; + } + } + this.batching = true; } @@ -73,14 +92,10 @@ namespace MLEM.Graphics { /// Ends batching. /// Call this method after calling Add or any of its overloads the desired number of times to add batched items. /// - /// The drawing order for sprite drawing. by default, since it is the best in terms of rendering performance. Note that is not supported. /// Thrown if this method is called before was called. - /// Thrown if the is , which is not supported. - public void EndBatch(SpriteSortMode sortMode = SpriteSortMode.Texture) { + public void EndBatch() { if (!this.batching) throw new InvalidOperationException("Not batching"); - if (sortMode == SpriteSortMode.Immediate) - throw new ArgumentOutOfRangeException(nameof(sortMode), "Cannot use sprite sort mode Immediate for static batching"); this.batching = false; // if we didn't add or remove any batch items, we don't have to recalculate anything @@ -90,41 +105,28 @@ namespace MLEM.Graphics { this.FilledBuffers = 0; this.textures.Clear(); - // order items according to the sort mode - IEnumerable ordered = this.items; - switch (sortMode) { - case SpriteSortMode.Texture: - // SortingKey is internal, but this will do for batching the same texture together - ordered = ordered.OrderBy(i => i.Texture.GetHashCode()); - break; - case SpriteSortMode.BackToFront: - ordered = ordered.OrderBy(i => -i.Depth); - break; - case SpriteSortMode.FrontToBack: - ordered = ordered.OrderBy(i => i.Depth); - break; - } - // fill vertex buffers var dataIndex = 0; Texture2D texture = null; - foreach (var item in ordered) { - // if the texture changes, we also have to start a new buffer! - if (dataIndex > 0 && (item.Texture != texture || dataIndex >= StaticSpriteBatch.Data.Length)) { - this.FillBuffer(this.FilledBuffers++, texture, StaticSpriteBatch.Data); - dataIndex = 0; + foreach (var itemSet in this.items.Values) { + foreach (var item in itemSet.Items) { + // if the texture changes, we also have to start a new buffer! + if (dataIndex > 0 && (item.Texture != texture || dataIndex >= StaticSpriteBatch.Data.Length)) { + this.FillBuffer(this.FilledBuffers++, texture, StaticSpriteBatch.Data); + dataIndex = 0; + } + StaticSpriteBatch.Data[dataIndex++] = item.TopLeft; + StaticSpriteBatch.Data[dataIndex++] = item.TopRight; + StaticSpriteBatch.Data[dataIndex++] = item.BottomLeft; + StaticSpriteBatch.Data[dataIndex++] = item.BottomRight; + texture = item.Texture; } - StaticSpriteBatch.Data[dataIndex++] = item.TopLeft; - StaticSpriteBatch.Data[dataIndex++] = item.TopRight; - StaticSpriteBatch.Data[dataIndex++] = item.BottomLeft; - StaticSpriteBatch.Data[dataIndex++] = item.BottomRight; - texture = item.Texture; } if (dataIndex > 0) this.FillBuffer(this.FilledBuffers++, texture, StaticSpriteBatch.Data); // ensure we have enough indices - var maxItems = Math.Min(this.items.Count, StaticSpriteBatch.MaxBatchItems); + var maxItems = Math.Min(this.itemAmount, StaticSpriteBatch.MaxBatchItems); // each item has 2 triangles which each have 3 indices if (this.indices == null || this.indices.IndexCount < 6 * maxItems) { var newIndices = new short[6 * maxItems]; @@ -178,7 +180,7 @@ namespace MLEM.Graphics { for (var i = 0; i < this.FilledBuffers; i++) { var buffer = this.vertexBuffers[i]; var texture = this.textures[i]; - var verts = Math.Min(this.items.Count * 4 - totalIndex, buffer.VertexCount); + var verts = Math.Min(this.itemAmount * 4 - totalIndex, buffer.VertexCount); this.graphicsDevice.SetVertexBuffer(buffer); if (effect != null) { @@ -352,6 +354,21 @@ namespace MLEM.Graphics { return this.Add(texture, destinationRectangle, null, color); } + /// + /// Adds an item to this batch. + /// Note that this batch needs to currently be batching, meaning has to have been called previously. + /// + /// The item to add. + /// The added , for chaining. + public Item Add(Item item) { + if (!this.batching) + throw new InvalidOperationException("Not batching"); + this.AddItemToSet(item); + this.itemAmount++; + this.batchChanged = true; + return item; + } + /// /// Removes the given item from this batch. /// Note that this batch needs to currently be batching, meaning has to have been called previously. @@ -362,7 +379,11 @@ namespace MLEM.Graphics { public bool Remove(Item item) { if (!this.batching) throw new InvalidOperationException("Not batching"); - if (this.items.Remove(item)) { + var key = item.GetSortKey(this.sortMode); + if (this.items.TryGetValue(key, out var itemSet) && itemSet.Remove(item)) { + if (itemSet.IsEmpty) + this.items.Remove(key); + this.itemAmount--; this.batchChanged = true; return true; } @@ -380,6 +401,7 @@ namespace MLEM.Graphics { this.items.Clear(); this.textures.Clear(); this.FilledBuffers = 0; + this.itemAmount = 0; this.batchChanged = true; } @@ -429,18 +451,13 @@ namespace MLEM.Graphics { } private Item Add(Texture2D texture, float depth, VertexPositionColorTexture tl, VertexPositionColorTexture tr, VertexPositionColorTexture bl, VertexPositionColorTexture br) { - if (!this.batching) - throw new InvalidOperationException("Not batching"); - var item = new Item(texture, depth, tl, tr, bl, br); - this.items.Add(item); - this.batchChanged = true; - return item; + return this.Add(new Item(texture, depth, tl, tr, bl, br)); } private void FillBuffer(int index, Texture2D texture, VertexPositionColorTexture[] data) { if (this.vertexBuffers.Count <= index) - this.vertexBuffers.Add(new VertexBuffer(this.graphicsDevice, VertexPositionColorTexture.VertexDeclaration, StaticSpriteBatch.MaxBatchItems * 4, BufferUsage.WriteOnly)); - this.vertexBuffers[index].SetData(data); + this.vertexBuffers.Add(new DynamicVertexBuffer(this.graphicsDevice, VertexPositionColorTexture.VertexDeclaration, StaticSpriteBatch.MaxBatchItems * 4, BufferUsage.WriteOnly)); + this.vertexBuffers[index].SetData(data, 0, data.Length, SetDataOptions.Discard); this.textures.Insert(index, texture); } @@ -452,6 +469,15 @@ namespace MLEM.Graphics { #endif } + private void AddItemToSet(Item item) { + var sortKey = item.GetSortKey(this.sortMode); + if (!this.items.TryGetValue(sortKey, out var itemSet)) { + itemSet = new ItemSet(); + this.items.Add(sortKey, itemSet); + } + itemSet.Add(item); + } + /// /// A struct that represents an item added to a using Add or any of its overloads. /// An item returned after adding can be removed using . @@ -474,6 +500,65 @@ namespace MLEM.Graphics { this.BottomRight = bottomRight; } + internal float GetSortKey(SpriteSortMode sortMode) { + switch (sortMode) { + case SpriteSortMode.Texture: + return this.Texture.GetHashCode(); + case SpriteSortMode.BackToFront: + return -this.Depth; + case SpriteSortMode.FrontToBack: + return this.Depth; + default: + return 0; + } + } + + } + + private class ItemSet { + + public IEnumerable Items { + get { + if (this.items != null) + return this.items; + if (this.single != null) + return Enumerable.Repeat(this.single, 1); + return Enumerable.Empty(); + } + } + public bool IsEmpty => this.items == null && this.single == null; + + private HashSet items; + private Item single; + + public void Add(Item item) { + if (this.items != null) { + this.items.Add(item); + } else if (this.single != null) { + this.items = new HashSet(); + this.items.Add(this.single); + this.items.Add(item); + this.single = null; + } else { + this.single = item; + } + } + + public bool Remove(Item item) { + if (this.items != null && this.items.Remove(item)) { + if (this.items.Count <= 1) { + this.single = this.items.Single(); + this.items = null; + } + return true; + } else if (this.single == item) { + this.single = null; + return true; + } else { + return false; + } + } + } #if FNA diff --git a/MLEM/Input/InputHandler.cs b/MLEM/Input/InputHandler.cs index 9856a7f..20e2bc5 100644 --- a/MLEM/Input/InputHandler.cs +++ b/MLEM/Input/InputHandler.cs @@ -20,6 +20,8 @@ namespace MLEM.Input { #else private static readonly int MaximumGamePadCount = GamePad.MaximumGamePadCount; #endif + private static readonly TouchLocation[] EmptyTouchLocations = new TouchLocation[0]; + private static readonly GenericInput[] EmptyGenericInputs = new GenericInput[0]; /// /// Contains all of the gestures that have finished during the last update call. @@ -83,20 +85,20 @@ namespace MLEM.Input { /// An array of all , and values that are currently down. /// Additionally, or can be used to determine the amount of time that a given input has been down for. /// - public GenericInput[] InputsDown { get; private set; } = Array.Empty(); + public GenericInput[] InputsDown { get; private set; } = InputHandler.EmptyGenericInputs; /// /// An array of all , and that are currently considered pressed. /// An input is considered pressed if it was up in the last update, and is up in the current one. /// - public GenericInput[] InputsPressed { get; private set; } = Array.Empty(); + public GenericInput[] InputsPressed { get; private set; } = InputHandler.EmptyGenericInputs; /// /// Contains the touch state from the last update call /// - public TouchCollection LastTouchState { get; private set; } = new TouchCollection(Array.Empty()); + public TouchCollection LastTouchState { get; private set; } = new TouchCollection(InputHandler.EmptyTouchLocations); /// /// Contains the current touch state /// - public TouchCollection TouchState { get; private set; } = new TouchCollection(Array.Empty()); + public TouchCollection TouchState { get; private set; } = new TouchCollection(InputHandler.EmptyTouchLocations); /// /// Contains the , but with the taken into account. /// @@ -281,7 +283,7 @@ namespace MLEM.Input { this.LastTouchState = this.TouchState; this.LastViewportTouchState = this.ViewportTouchState; - this.TouchState = active ? TouchPanel.GetState() : new TouchCollection(Array.Empty()); + this.TouchState = active ? TouchPanel.GetState() : new TouchCollection(InputHandler.EmptyTouchLocations); if (this.TouchState.Count > 0 && this.ViewportOffset != Point.Zero) { this.ViewportTouchState = new List(); foreach (var touch in this.TouchState) { @@ -302,8 +304,8 @@ namespace MLEM.Input { } if (this.inputsDownAccum.Count <= 0 && this.inputsDown.Count <= 0) { - this.InputsPressed = Array.Empty(); - this.InputsDown = Array.Empty(); + this.InputsPressed = InputHandler.EmptyGenericInputs; + this.InputsDown = InputHandler.EmptyGenericInputs; } else { // handle pressed inputs var pressed = new List(); @@ -823,6 +825,7 @@ namespace MLEM.Input { downTime = DateTime.UtcNow - start; return true; } + downTime = default; return false; } @@ -851,6 +854,7 @@ namespace MLEM.Input { upTime = DateTime.UtcNow - start; return true; } + upTime = default; return false; } @@ -879,6 +883,7 @@ namespace MLEM.Input { lastPressTime = DateTime.UtcNow - start; return true; } + lastPressTime = default; return false; } diff --git a/MLEM/Input/Keybind.cs b/MLEM/Input/Keybind.cs index e4f0b8c..78ad1b4 100644 --- a/MLEM/Input/Keybind.cs +++ b/MLEM/Input/Keybind.cs @@ -3,6 +3,10 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; +#if NET452 +using MLEM.Extensions; +#endif + namespace MLEM.Input { /// /// A keybind represents a generic way to trigger input. @@ -13,8 +17,10 @@ namespace MLEM.Input { [DataContract] public class Keybind : IComparable, IComparable { + private static readonly Combination[] EmptyCombinations = new Combination[0]; + [DataMember] - private Combination[] combinations = Array.Empty(); + private Combination[] combinations = Keybind.EmptyCombinations; /// /// Creates a new keybind and adds the given key and modifiers using @@ -77,7 +83,7 @@ namespace MLEM.Input { /// /// This keybind, for chaining public Keybind Clear() { - this.combinations = Array.Empty(); + this.combinations = Keybind.EmptyCombinations; return this; } diff --git a/MLEM/MLEM.FNA.csproj b/MLEM/MLEM.FNA.csproj index 2e28813..9a83102 100644 --- a/MLEM/MLEM.FNA.csproj +++ b/MLEM/MLEM.FNA.csproj @@ -1,6 +1,6 @@  - netstandard2.0 + net452;netstandard2.0;net6.0 true true MLEM @@ -20,9 +20,11 @@ - + all + + diff --git a/MLEM/MLEM.csproj b/MLEM/MLEM.csproj index 6eb80b7..ac0df57 100644 --- a/MLEM/MLEM.csproj +++ b/MLEM/MLEM.csproj @@ -1,6 +1,6 @@  - netstandard2.0 + net452;netstandard2.0;net6.0 true true @@ -21,6 +21,8 @@ all + + diff --git a/MLEM/Misc/EnumHelper.cs b/MLEM/Misc/EnumHelper.cs index 645f2e8..684aa19 100644 --- a/MLEM/Misc/EnumHelper.cs +++ b/MLEM/Misc/EnumHelper.cs @@ -22,8 +22,12 @@ namespace MLEM.Misc { /// /// The type whose enum to get /// An enumerable of the values of the enum, in declaration order. - public static T[] GetValues() { + public static T[] GetValues() where T : struct, Enum { + #if NET6_0_OR_GREATER + return Enum.GetValues(); + #else return (T[]) Enum.GetValues(typeof(T)); + #endif } } diff --git a/MLEM/Misc/GenericDataHolder.cs b/MLEM/Misc/GenericDataHolder.cs index 14decf2..b224b55 100644 --- a/MLEM/Misc/GenericDataHolder.cs +++ b/MLEM/Misc/GenericDataHolder.cs @@ -11,6 +11,8 @@ namespace MLEM.Misc { [DataContract] public class GenericDataHolder : IGenericDataHolder { + private static readonly string[] EmptyStrings = new string[0]; + [DataMember(EmitDefaultValue = false)] private Dictionary data; @@ -34,9 +36,9 @@ namespace MLEM.Misc { } /// - public IReadOnlyCollection GetDataKeys() { + public IEnumerable GetDataKeys() { if (this.data == null) - return Array.Empty(); + return GenericDataHolder.EmptyStrings; return this.data.Keys; } @@ -67,7 +69,7 @@ namespace MLEM.Misc { /// Returns all of the generic data that this object stores. /// /// The generic data on this object - IReadOnlyCollection GetDataKeys(); + IEnumerable GetDataKeys(); } } diff --git a/README.md b/README.md index 6cc3305..04152e1 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ **MLEM Library for Extending MonoGame and FNA** is a set of multipurpose libraries for the game frameworks [MonoGame](https://www.monogame.net/) and [FNA](https://fna-xna.github.io/) that provides abstractions, quality of life improvements and additional features like an extensive ui system and easy input handling. +MLEM is platform-agnostic and multi-targets .NET Standard 2.0, .NET 6.0 and .NET Framework 4.5.2, which makes it compatible with MonoGame and FNA on Desktop, mobile devices and consoles. + # What next? - Get it on [NuGet](https://www.nuget.org/packages?q=mlem) - Get prerelease builds on [BaGet](https://nuget.ellpeck.de/?q=mlem) diff --git a/Sandbox/GameImpl.cs b/Sandbox/GameImpl.cs index e66e532..7314feb 100644 --- a/Sandbox/GameImpl.cs +++ b/Sandbox/GameImpl.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Text.RegularExpressions; using FontStashSharp; @@ -13,6 +14,7 @@ using MLEM.Extensions; using MLEM.Font; using MLEM.Formatting; using MLEM.Formatting.Codes; +using MLEM.Graphics; using MLEM.Input; using MLEM.Misc; using MLEM.Startup; @@ -351,6 +353,35 @@ public class GameImpl : MlemGame { }); } this.UiSystem.Add("WidthTest", widthPanel); + + var batch = new StaticSpriteBatch(this.GraphicsDevice); + batch.BeginBatch(); + var depth = 0F; + var items = new List(); + foreach (var r in atlas.Regions) + items.Add(batch.Add(r, new Vector2(50 + r.GetHashCode() % 200, 50), ColorHelper.FromHexRgb(r.GetHashCode()), 0, Vector2.Zero, 1, SpriteEffects.None, depth += 0.0001F)); + batch.EndBatch(); + var sortMode = SpriteSortMode.Deferred; + this.OnUpdate += (_, _) => { + if (MlemGame.Input.IsPressed(Keys.S)) { + do { + sortMode = (SpriteSortMode) (((int) sortMode + 1) % 5); + } while (sortMode == SpriteSortMode.Immediate); + Console.WriteLine(sortMode); + batch.BeginBatch(sortMode); + batch.EndBatch(); + } else { + for (var i = 0; i < items.Count; i++) { + if (MlemGame.Input.IsPressed(Keys.D1 + i)) { + batch.BeginBatch(); + if (!batch.Remove(items[i])) + batch.Add(items[i]); + batch.EndBatch(); + } + } + } + }; + this.OnDraw += (_, _) => batch.Draw(null, SamplerState.PointClamp, null, null, null, Matrix.CreateScale(3)); } protected override void DoUpdate(GameTime gameTime) { diff --git a/Sandbox/Sandbox.csproj b/Sandbox/Sandbox.csproj index ae66e79..b7ba249 100644 --- a/Sandbox/Sandbox.csproj +++ b/Sandbox/Sandbox.csproj @@ -1,10 +1,11 @@  - + Exe net6.0 + false - + @@ -12,7 +13,7 @@ - + @@ -21,7 +22,7 @@ - + diff --git a/Tests/Content/Texture.atlas b/Tests/Content/Texture.atlas index fd90b79..a3c47f2 100644 --- a/Tests/Content/Texture.atlas +++ b/Tests/Content/Texture.atlas @@ -13,11 +13,21 @@ TestRegionNegativePivot loc 0 32 +16 16 piv -32 +46 +DataTest +loc 0 0 16 16 +dat DataPoint1 ThisIsSomeData +dat DataPoint2 3.5 +dat DataPoint3 --- + LongTableUp -loc 0 32 64 48 -piv 16 48 +piv 16 48 loc 0 32 64 48 +copy Copy1 16 0 +cpy Copy2 32 4 + +Copy3 from +LongTableUp off 2 4 LongTableRight LongTableDown LongTableLeft -loc 32 30 64 48 +location 32 30 64 48 piv 80 46 -off 32 2 +offset 32 2 diff --git a/Tests/DataTextureAtlasTests.cs b/Tests/DataTextureAtlasTests.cs index 551e9ae..f81c344 100644 --- a/Tests/DataTextureAtlasTests.cs +++ b/Tests/DataTextureAtlasTests.cs @@ -9,26 +9,51 @@ namespace Tests { public class TestDataTextureAtlas { [Test] - public void Test() { + public void Test([Values(0, 4)] int regionX, [Values(0, 4)] int regionY) { using var game = TestGame.Create(); using var texture = new Texture2D(game.GraphicsDevice, 1, 1); - var atlas = DataTextureAtlas.LoadAtlasData(new TextureRegion(texture), game.RawContent, "Texture.atlas"); - Assert.AreEqual(atlas.Regions.Count(), 8); + var region = new TextureRegion(texture, regionX, regionY, 1, 1); + var atlas = DataTextureAtlas.LoadAtlasData(region, game.RawContent, "Texture.atlas"); + Assert.AreEqual(12, atlas.Regions.Count()); + + // no pivot + var plant = atlas["Plant"]; + Assert.AreEqual(plant.Area, new Rectangle(96 + regionX, 0 + regionY, 16, 32)); + Assert.AreEqual(plant.PivotPixels, Vector2.Zero); // no added offset var table = atlas["LongTableUp"]; - Assert.AreEqual(table.Area, new Rectangle(0, 32, 64, 48)); + Assert.AreEqual(table.Area, new Rectangle(0 + regionX, 32 + regionY, 64, 48)); Assert.AreEqual(table.PivotPixels, new Vector2(16, 48 - 32)); // added offset - var table2 = atlas["LongTableLeft"]; - Assert.AreEqual(table2.Area, new Rectangle(64, 32, 64, 48)); + var table2 = atlas["LongTableDown"]; + Assert.AreEqual(table2.Area, new Rectangle(64 + regionX, 32 + regionY, 64, 48)); Assert.AreEqual(table2.PivotPixels, new Vector2(112 - 64, 48 - 32)); // negative pivot var negativePivot = atlas["TestRegionNegativePivot"]; - Assert.AreEqual(negativePivot.Area, new Rectangle(0, 32, 16, 16)); + Assert.AreEqual(negativePivot.Area, new Rectangle(0 + regionX, 32 + regionY, 16, 16)); Assert.AreEqual(negativePivot.PivotPixels, new Vector2(-32, 46 - 32)); + + // cpy (pivot pixels should be identical to LongTableUp because they're region-internal) + var copy1 = atlas["Copy1"]; + Assert.AreEqual(copy1.Area, new Rectangle(0 + 16 + regionX, 32 + regionY, 64, 48)); + Assert.AreEqual(copy1.PivotPixels, new Vector2(16, 48 - 32)); + var copy2 = atlas["Copy2"]; + Assert.AreEqual(copy2.Area, new Rectangle(0 + 32 + regionX, 32 + 4 + regionY, 64, 48)); + Assert.AreEqual(copy2.PivotPixels, new Vector2(16, 48 - 32)); + + // frm + var copy3 = atlas["Copy3"]; + Assert.AreEqual(copy3.Area, new Rectangle(0 + 2 + regionX, 32 + 4 + regionY, 64, 48)); + Assert.AreEqual(copy3.PivotPixels, new Vector2(16, 48 - 32)); + + // data + var data = atlas["DataTest"]; + Assert.AreEqual("ThisIsSomeData", data.GetData("DataPoint1")); + Assert.AreEqual("3.5", data.GetData("DataPoint2")); + Assert.AreEqual("---", data.GetData("DataPoint3")); } } diff --git a/Tests/Tests.FNA.csproj b/Tests/Tests.FNA.csproj index 8f1fe64..0f9cdad 100644 --- a/Tests/Tests.FNA.csproj +++ b/Tests/Tests.FNA.csproj @@ -1,9 +1,11 @@ - net5.0 + net6.0 nunit + TestResults.FNA Tests $(DefineConstants);FNA + false diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index 6f1da92..4386917 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -1,16 +1,18 @@ - net5.0 + net6.0 nunit + TestResults + false - + - + @@ -23,7 +25,7 @@ - + PreserveNewest diff --git a/build.cake b/build.cake index 5b44f4e..68882dd 100644 --- a/build.cake +++ b/build.cake @@ -8,7 +8,8 @@ var branch = Argument("branch", "main"); var config = Argument("configuration", "Release"); Task("Prepare").Does(() => { - DotNetCoreRestore("MLEM.sln"); + DotNetRestore("MLEM.sln"); + DotNetRestore("MLEM.FNA.sln"); if (branch != "release") { var buildNum = EnvironmentVariable("BUILD_NUMBER"); @@ -16,36 +17,34 @@ Task("Prepare").Does(() => { version += "-" + buildNum; } - DeleteFiles("**/*.nupkg"); + DeleteFiles("**/MLEM*.nupkg"); }); Task("Build").IsDependentOn("Prepare").Does(() =>{ - var settings = new DotNetCoreBuildSettings { + var settings = new DotNetBuildSettings { Configuration = config, ArgumentCustomization = args => args.Append($"/p:Version={version}") }; - foreach (var project in GetFiles("**/MLEM*.csproj")) - DotNetCoreBuild(project.FullPath, settings); - DotNetCoreBuild("Demos/Demos.csproj", settings); - DotNetCoreBuild("Demos/Demos.FNA.csproj", settings); + DotNetBuild("MLEM.sln", settings); + DotNetBuild("MLEM.FNA.sln", settings); }); Task("Test").IsDependentOn("Build").Does(() => { - var settings = new DotNetCoreTestSettings { + var settings = new DotNetTestSettings { Configuration = config, Collectors = {"XPlat Code Coverage"} }; - DotNetCoreTest("Tests/Tests.csproj", settings); - DotNetCoreTest("Tests/Tests.FNA.csproj", settings); + DotNetTest("MLEM.sln", settings); + DotNetTest("MLEM.FNA.sln", settings); }); Task("Pack").IsDependentOn("Test").Does(() => { - var settings = new DotNetCorePackSettings { + var settings = new DotNetPackSettings { Configuration = config, ArgumentCustomization = args => args.Append($"/p:Version={version}") }; - foreach (var project in GetFiles("**/MLEM*.csproj")) - DotNetCorePack(project.FullPath, settings); + DotNetPack("MLEM.sln", settings); + DotNetPack("MLEM.FNA.sln", settings); }); Task("Push").WithCriteria(branch == "main" || branch == "release").IsDependentOn("Pack").Does(() => { @@ -62,7 +61,7 @@ Task("Push").WithCriteria(branch == "main" || branch == "release").IsDependentOn }; } settings.SkipDuplicate = true; - NuGetPush(GetFiles("**/*.nupkg"), settings); + NuGetPush(GetFiles("**/MLEM*.nupkg"), settings); }); Task("Document").Does(() => {