diff --git a/.githooks/pre-push b/.githooks/pre-push new file mode 100644 index 0000000..d6a3ebf --- /dev/null +++ b/.githooks/pre-push @@ -0,0 +1,8 @@ +#!/bin/sh +command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting the 'pre-push' file in the hooks directory (set by 'core.hookspath'; usually '.git/hooks').\n"; exit 2; } +git lfs pre-push "$@" + +if ! git diff origin --name-status | grep -E -q "M\s+CHANGELOG.md"; then + echo "The changelog was not updated. Please document your changes in CHANGELOG.md before pushing." + exit 1 +fi diff --git a/.github/workflows/enforce-changelog.yml b/.github/workflows/enforce-changelog.yml new file mode 100644 index 0000000..94ac6c4 --- /dev/null +++ b/.github/workflows/enforce-changelog.yml @@ -0,0 +1,11 @@ +on: pull_request +jobs: + enforce-changelog: + runs-on: ubuntu-latest + steps: + - uses: dangoslen/changelog-enforcer@v3 + with: + changeLogPath: CHANGELOG.md + missingUpdateErrorMessage: | + The changelog was not updated. Please document your changes in CHANGELOG.md. + Run `git config core.hooksPath .githooks` to enable a git hook that ensures you updated the changelog before pushing. diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..5f3ecaa --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,51 @@ +on: [push, pull_request] +jobs: + build-publish: + runs-on: ubuntu-latest + steps: + - name: Clone repository + uses: actions/checkout@v4 + with: + submodules: recursive + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '8.0.x' + - name: Setup Java + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + - name: Restore tools + run: dotnet tool restore + - name: Run cake + uses: coactions/setup-xvfb@v1 + with: + run: dotnet cake --target Publish --branch ${{ github.ref_name }} + env: + NUGET_KEY: ${{ secrets.NUGET_KEY }} + BAGET_KEY: ${{ secrets.BAGET_KEY }} + docs: + runs-on: ubuntu-latest + steps: + - name: Clone repository + uses: actions/checkout@v4 + with: + submodules: recursive + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '8.0.x' + - name: Restore tools + run: dotnet tool restore + - name: Run cake + run: dotnet cake --target Document --branch $GITHUB_REF_NAME + - name: Deploy + if: github.event_name == 'push' && github.ref_name == 'release' + # this is a beautiful way to deploy a website and i will not take any criticism + run: | + curl -L --output cloudflared.deb https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb && sudo dpkg -i cloudflared.deb + mkdir ~/.ssh && echo "${{ secrets.ELLBOT_KEY }}" > ~/.ssh/id_rsa && chmod 600 ~/.ssh/id_rsa + rsync -rv --delete -e 'ssh -o "ProxyCommand cloudflared access ssh --hostname %h" -o "StrictHostKeyChecking=no"' Docs/_site/. ellbot@ssh.ellpeck.de:/var/www/MLEM diff --git a/.gitmodules b/.gitmodules index 6f98b12..7515ff9 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,6 @@ [submodule "FNA"] - path = FNA + path = ThirdParty/FNA url = https://github.com/FNA-XNA/FNA [submodule "FontStashSharp"] - path = FontStashSharp + path = ThirdParty/FontStashSharp url = https://github.com/FontStashSharp/FontStashSharp diff --git a/CHANGELOG.md b/CHANGELOG.md index ffb1896..3e0b67c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ MLEM tries to adhere to [semantic versioning](https://semver.org/). Potentially breaking changes are written in **bold**. Jump to version: +- [6.3.0](#630) - [6.2.0](#620) - [6.1.0](#610) - [6.0.0](#600) @@ -10,6 +11,57 @@ Jump to version: - [5.1.0](#510) - [5.0.0](#500) +## 6.3.0 + +### MLEM +Additions +- Added GraphicsExtensions.WithRenderTargets, a multi-target version of WithRenderTarget +- Added Zero, One, Linear and Clamp to Easings +- Added GetRandomEntry and GetRandomWeightedEntry to SingleRandom +- Added the ability to draw single corners of AutoTiling's extended auto tiles +- Added ColorHelper.TryFromHexString, a non-throwing version of FromHexString +- Added ToHexStringRgba and ToHexStringRgb to ColorExtensions + +Improvements +- Stopped the text formatter throwing if a color can't be parsed +- Improved text formatter tokenization performance +- Allow using control and arrow keys to move the visible area of a text input +- Allow formatting codes applied later to override settings of earlier ones + +Fixes +- Fixed TextInput not working correctly when using surrogate pairs +- Fixed InputHandler touch states being initialized incorrectly when touch handling is disabled +- Fixed empty NinePatch regions stalling when using tile mode +- Fixed bold and italic formatting code closing tags working on each other + +### MLEM.Ui +Additions +- Added UiControls.NavType, which stores the most recently used type of ui navigation +- Added SetWidthBasedOnAspect and SetHeightBasedOnAspect to images +- Added the ability to set a custom SamplerState for images +- Added some useful additional constructors to various elements + +Improvements +- Allow scrolling panels to contain other scrolling panels +- Allow dropdowns to have scrolling panels +- Improved Panel performance when adding and removing a lot of children +- Don't reset the caret position of a text field when selecting or deselecting it +- Improved UiParser.ParseImage with locks and a callback action + +Fixes +- Fixed panels updating their relevant children too much when the scroll bar is hidden +- Fixed a stack overflow exception when a panel's scroll bar auto-hiding causes elements to gain height +- Fixed scrolling panels calculating their height incorrectly when their first child is hidden + +### MLEM.Extended +Improvements +- Updated to FontStashSharp 1.3.0's API +- Expose character and line spacing in GenericStashFont + +### MLEM.Data +Fixes +- Fixed various exception types not being wrapped by ContentLoadExceptions when loading raw or JSON content + ## 6.2.0 ### MLEM diff --git a/Demos.Android/Activity1.cs b/Demos.Android/Activity1.cs index 1521e39..af0713d 100644 --- a/Demos.Android/Activity1.cs +++ b/Demos.Android/Activity1.cs @@ -48,10 +48,10 @@ public class Activity1 : AndroidGameActivity { base.OnWindowFocusChanged(hasFocus); // hide the status bar if (hasFocus) { -#pragma warning disable CA1422 +#pragma warning disable CS0618 // TODO this is deprecated, find out how to replace it this.Window.DecorView.SystemUiVisibility = (StatusBarVisibility) (SystemUiFlags.ImmersiveSticky | SystemUiFlags.LayoutStable | SystemUiFlags.LayoutHideNavigation | SystemUiFlags.LayoutFullscreen | SystemUiFlags.HideNavigation | SystemUiFlags.Fullscreen); -#pragma warning restore CA1422 +#pragma warning restore CS0618 } } diff --git a/Demos.Android/Demos.Android.csproj b/Demos.Android/Demos.Android.csproj index c915abd..6cf9df3 100644 --- a/Demos.Android/Demos.Android.csproj +++ b/Demos.Android/Demos.Android.csproj @@ -1,6 +1,6 @@ - net7.0-android + net8.0-android 31 Exe de.ellpeck.mlem.demos.android diff --git a/Demos.DesktopGL/Demos.DesktopGL.FNA.csproj b/Demos.DesktopGL/Demos.DesktopGL.FNA.csproj index 8129d37..80e79e8 100644 --- a/Demos.DesktopGL/Demos.DesktopGL.FNA.csproj +++ b/Demos.DesktopGL/Demos.DesktopGL.FNA.csproj @@ -2,7 +2,7 @@ Exe - net7.0 + net8.0 Icon.ico MLEM Desktop Demos Demos.DesktopGL @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + PreserveNewest %(Filename)%(Extension) diff --git a/Demos.DesktopGL/Demos.DesktopGL.csproj b/Demos.DesktopGL/Demos.DesktopGL.csproj index f861a8c..0663b6c 100644 --- a/Demos.DesktopGL/Demos.DesktopGL.csproj +++ b/Demos.DesktopGL/Demos.DesktopGL.csproj @@ -2,7 +2,7 @@ Exe - net7.0 + net8.0 Icon.ico MLEM Desktop Demos false @@ -26,9 +26,4 @@ - - - - - diff --git a/Demos/Content/Markdown.md b/Demos/Content/Markdown.md index 76b495b..56d64b1 100644 --- a/Demos/Content/Markdown.md +++ b/Demos/Content/Markdown.md @@ -12,12 +12,16 @@ Strikethrough with ~~two tildes~~. [I'm an inline-style link](https://www.google.com) +Logo: ![](https://raw.githubusercontent.com/Ellpeck/MLEM/main/Media/Logo.png) +Wide logo: +![](https://raw.githubusercontent.com/Ellpeck/MLEM/main/Media/Banner.png) + Some `inline code` right here ```js function codeBlock() { } -``` \ No newline at end of file +``` diff --git a/Demos/Demos.FNA.csproj b/Demos/Demos.FNA.csproj index a15422e..9af1d93 100644 --- a/Demos/Demos.FNA.csproj +++ b/Demos/Demos.FNA.csproj @@ -1,7 +1,7 @@  - netstandard2.0 + net8.0 Demos $(DefineConstants);FNA false @@ -14,7 +14,7 @@ - + all diff --git a/Demos/Demos.csproj b/Demos/Demos.csproj index 31946ae..fd1516b 100644 --- a/Demos/Demos.csproj +++ b/Demos/Demos.csproj @@ -1,7 +1,7 @@  - netstandard2.0 + net8.0 false diff --git a/Demos/TextFormattingDemo.cs b/Demos/TextFormattingDemo.cs index 95d1354..1d32021 100644 --- a/Demos/TextFormattingDemo.cs +++ b/Demos/TextFormattingDemo.cs @@ -15,7 +15,7 @@ namespace Demos { private const string Text = "MLEM's text formatting system allows for various formatting codes to be applied in the middle of a string. Here's a demonstration of some of them.\n\n" + - "You can write in bold, italics, with an underline, strikethrough, with a drop shadow whose color and offset you can modify in each application of the code, with an outline that you can also modify dynamically, or with various types of combined formatting codes.\n\n" + + "You can write in bold, italics, with an underline, strikethrough, with a drop shadow whose color and offset you can modify in each application of the code, with an outline that you can also modify dynamically, or with various types of combined formatting codes.\n\n" + "You can apply custom colors to text, including all default MonoGame colors and inline custom colors.\n\n" + "You can also use animations like a wobbly one, as well as create custom ones using the Code class.\n\n" + "You can also display icons in your text, and use superscript or subscript formatting!\n\n" + diff --git a/Demos/UiDemo.cs b/Demos/UiDemo.cs index a3381c0..934d010 100644 --- a/Demos/UiDemo.cs +++ b/Demos/UiDemo.cs @@ -223,6 +223,15 @@ namespace Demos { PositionOffset = new Vector2(0, 1) }); + var subPanel = this.root.AddChild(new Panel(Anchor.AutoLeft, new Vector2(1, 25), Vector2.Zero, false, true) { + PositionOffset = new Vector2(0, 1), + Texture = null, + ChildPadding = Padding.Empty + }); + subPanel.AddChild(new Paragraph(Anchor.AutoLeft, 1, "This is a nested scrolling panel!")); + for (var i = 1; i <= 5; i++) + subPanel.AddChild(new Button(Anchor.AutoLeft, new Vector2(1, 10), $"Button {i}") {PositionOffset = new Vector2(0, 1)}); + const string alignText = "Paragraphs can have left aligned text, right aligned text and center aligned text."; this.root.AddChild(new VerticalSpace(3)); var alignPar = this.root.AddChild(new Paragraph(Anchor.AutoLeft, 1, alignText)); diff --git a/Docs/docfx.json b/Docs/docfx.json index 54b7cfa..eadf6c2 100644 --- a/Docs/docfx.json +++ b/Docs/docfx.json @@ -49,7 +49,7 @@ "globalMetadata": { "_appTitle": "MLEM Documentation", "_appLogoPath": "Logo.svg", - "_appFooter": "© 2019-2023 EllpeckImpressumPrivacyStatus", + "_appFooter": "© 2019-2024 EllpeckImpressumPrivacyStatus", "_enableSearch": true }, "dest": "_site", diff --git a/FNA b/FNA deleted file mode 160000 index 697cc63..0000000 --- a/FNA +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 697cc63662914c0dc26c500bc9b8498b5ca8a68f diff --git a/FnaNative/FAudio.dll b/FnaNative/FAudio.dll deleted file mode 100644 index 8a08f43..0000000 Binary files a/FnaNative/FAudio.dll and /dev/null differ diff --git a/FnaNative/FNA3D.dll b/FnaNative/FNA3D.dll deleted file mode 100644 index 6573c01..0000000 Binary files a/FnaNative/FNA3D.dll and /dev/null differ diff --git a/FnaNative/libFAudio.0.dylib b/FnaNative/libFAudio.0.dylib deleted file mode 100644 index 9fa215b..0000000 Binary files a/FnaNative/libFAudio.0.dylib and /dev/null differ diff --git a/FnaNative/libFAudio.so.0 b/FnaNative/libFAudio.so.0 deleted file mode 100644 index f9659de..0000000 Binary files a/FnaNative/libFAudio.so.0 and /dev/null differ diff --git a/FnaNative/libFNA3D.0.dylib b/FnaNative/libFNA3D.0.dylib deleted file mode 100644 index 92e64e3..0000000 Binary files a/FnaNative/libFNA3D.0.dylib and /dev/null differ diff --git a/FnaNative/libFNA3D.so.0 b/FnaNative/libFNA3D.so.0 deleted file mode 100644 index 1db8437..0000000 Binary files a/FnaNative/libFNA3D.so.0 and /dev/null differ diff --git a/FnaNative/libSDL2-2.0.0.dylib b/FnaNative/libSDL2-2.0.0.dylib deleted file mode 100644 index cf8e6d4..0000000 Binary files a/FnaNative/libSDL2-2.0.0.dylib and /dev/null differ diff --git a/FnaNative/libSDL2-2.0.so.0 b/FnaNative/libSDL2-2.0.so.0 deleted file mode 100644 index c76d104..0000000 Binary files a/FnaNative/libSDL2-2.0.so.0 and /dev/null differ diff --git a/FontStashSharp b/FontStashSharp deleted file mode 160000 index f11f97b..0000000 --- a/FontStashSharp +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f11f97b709e50960dd8ce1f727974744c4f8a0dd diff --git a/LICENSE b/LICENSE index f825382..f9314b4 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019-2023 Ellpeck +Copyright (c) 2019-2024 Ellpeck Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/MLEM.Data/Content/RawContentManager.cs b/MLEM.Data/Content/RawContentManager.cs index 34bb8ab..67aa92e 100644 --- a/MLEM.Data/Content/RawContentManager.cs +++ b/MLEM.Data/Content/RawContentManager.cs @@ -100,7 +100,7 @@ namespace MLEM.Data.Content { r.Name = assetName; return t; } - } catch (FileNotFoundException) {} + } catch (IOException) {} } } throw new ContentLoadException($"Asset {assetName} not found. Tried files {string.Join(", ", triedFiles)}"); diff --git a/MLEM.Data/ContentExtensions.cs b/MLEM.Data/ContentExtensions.cs index 96fce9c..943d94f 100644 --- a/MLEM.Data/ContentExtensions.cs +++ b/MLEM.Data/ContentExtensions.cs @@ -68,7 +68,7 @@ namespace MLEM.Data { using (var reader = new JsonTextReader(stream)) return serializerToUse.Deserialize(reader); } - } catch (FileNotFoundException) {} + } catch (IOException) {} } throw new ContentLoadException($"Asset {name} not found. Tried files {string.Join(", ", triedFiles)}"); } diff --git a/MLEM.Data/MLEM.Data.FNA.csproj b/MLEM.Data/MLEM.Data.FNA.csproj index 00569c4..f2957ee 100644 --- a/MLEM.Data/MLEM.Data.FNA.csproj +++ b/MLEM.Data/MLEM.Data.FNA.csproj @@ -1,9 +1,9 @@  - net452;netstandard2.0;net7.0 + net452;netstandard2.0;net8.0 true true - true + true MLEM.Data $(DefineConstants);FNA NU1701 @@ -28,14 +28,14 @@ all - + all all - + all diff --git a/MLEM.Data/MLEM.Data.csproj b/MLEM.Data/MLEM.Data.csproj index bce3939..b185a54 100644 --- a/MLEM.Data/MLEM.Data.csproj +++ b/MLEM.Data/MLEM.Data.csproj @@ -1,9 +1,9 @@  - net452;netstandard2.0;net7.0 + net452;netstandard2.0;net8.0 true true - true + true NU1701 diff --git a/MLEM.Extended/Font/GenericStashFont.cs b/MLEM.Extended/Font/GenericStashFont.cs index a6f2b9e..961e13e 100644 --- a/MLEM.Extended/Font/GenericStashFont.cs +++ b/MLEM.Extended/Font/GenericStashFont.cs @@ -11,6 +11,7 @@ namespace MLEM.Extended.Font { /// The that is being wrapped by this generic font /// public readonly SpriteFontBase Font; + /// public override GenericFont Bold { get; } /// @@ -18,6 +19,15 @@ namespace MLEM.Extended.Font { /// public override float LineHeight => this.Font.LineHeight; + /// + /// The character spacing that will be passed to the underlying . + /// + public float CharacterSpacing { get; set; } + /// + /// The line spacing that will be passed to the underlying . + /// + public float LineSpacing { get; set; } + /// /// Creates a new generic font using . /// Optionally, a bold and italic version of the font can be supplied. @@ -33,12 +43,12 @@ namespace MLEM.Extended.Font { /// protected override float MeasureCharacter(int codePoint) { - return this.Font.MeasureString(CodePointSource.ToString(codePoint)).X; + return this.Font.MeasureString(CodePointSource.ToString(codePoint), null, this.CharacterSpacing, this.LineSpacing).X; } /// protected override void DrawCharacter(SpriteBatch batch, int codePoint, string character, Vector2 position, Color color, float rotation, Vector2 scale, SpriteEffects effects, float layerDepth) { - this.Font.DrawText(batch, character, position, color, scale, rotation, Vector2.Zero, layerDepth); + this.Font.DrawText(batch, character, position, color, rotation, Vector2.Zero, scale, layerDepth, this.CharacterSpacing, this.LineSpacing); } } diff --git a/MLEM.Extended/MLEM.Extended.FNA.csproj b/MLEM.Extended/MLEM.Extended.FNA.csproj index a4e90a4..8d830ad 100644 --- a/MLEM.Extended/MLEM.Extended.FNA.csproj +++ b/MLEM.Extended/MLEM.Extended.FNA.csproj @@ -1,9 +1,9 @@  - netstandard2.0;net7.0 + netstandard2.0;net8.0 true true - true + true MLEM.Extended $(DefineConstants);FNA NU1702 @@ -24,10 +24,10 @@ - + all - + all diff --git a/MLEM.Extended/MLEM.Extended.csproj b/MLEM.Extended/MLEM.Extended.csproj index 8c45f4e..6d99ba2 100644 --- a/MLEM.Extended/MLEM.Extended.csproj +++ b/MLEM.Extended/MLEM.Extended.csproj @@ -1,9 +1,9 @@  - netstandard2.0;net7.0 + netstandard2.0;net8.0 true true - true + true @@ -27,7 +27,7 @@ all - + all diff --git a/MLEM.Extended/Tiled/TiledExtensions.cs b/MLEM.Extended/Tiled/TiledExtensions.cs index b933636..cb1d575 100644 --- a/MLEM.Extended/Tiled/TiledExtensions.cs +++ b/MLEM.Extended/Tiled/TiledExtensions.cs @@ -44,7 +44,8 @@ namespace MLEM.Extended.Tiled { /// The key by which to get a property /// The color property public static Color GetColor(this TiledMapProperties properties, string key) { - return ColorHelper.FromHexString(properties.Get(key)); + ColorHelper.TryFromHexString(properties.Get(key), out var val); + return val; } /// diff --git a/MLEM.FNA.sln b/MLEM.FNA.sln index d4e2539..f1f9d6b 100644 --- a/MLEM.FNA.sln +++ b/MLEM.FNA.sln @@ -16,11 +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", "FNA\FNA.csproj", "{35253CE1-C864-4CD3-8249-4D1319748E8F}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FNA", "ThirdParty\FNA\FNA.csproj", "{35253CE1-C864-4CD3-8249-4D1319748E8F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FontStashSharp.FNA", "FontStashSharp\src\XNA\FontStashSharp.FNA.csproj", "{39249E92-EBF2-4951-A086-AB4951C3CCE1}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FontStashSharp.FNA", "ThirdParty\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}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FNA.Core", "ThirdParty\FNA\FNA.Core.csproj", "{458FFA5E-A1C4-4B23-A5D8-259385FEECED}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/MLEM.Startup/MLEM.Startup.FNA.csproj b/MLEM.Startup/MLEM.Startup.FNA.csproj index 7e118ef..c8d5777 100644 --- a/MLEM.Startup/MLEM.Startup.FNA.csproj +++ b/MLEM.Startup/MLEM.Startup.FNA.csproj @@ -1,10 +1,10 @@  - net452;netstandard2.0;net7.0 + net452;netstandard2.0;net8.0 true true - true + true MLEM.Startup $(DefineConstants);FNA @@ -22,11 +22,11 @@ - + - + all diff --git a/MLEM.Startup/MLEM.Startup.csproj b/MLEM.Startup/MLEM.Startup.csproj index 4d22b29..23a3991 100644 --- a/MLEM.Startup/MLEM.Startup.csproj +++ b/MLEM.Startup/MLEM.Startup.csproj @@ -1,10 +1,10 @@  - net452;netstandard2.0;net7.0 + net452;netstandard2.0;net8.0 true true - true + true diff --git a/MLEM.Templates/MLEM.Templates.csproj b/MLEM.Templates/MLEM.Templates.csproj index 8bc7684..8c9895a 100644 --- a/MLEM.Templates/MLEM.Templates.csproj +++ b/MLEM.Templates/MLEM.Templates.csproj @@ -1,12 +1,11 @@  - net452;netstandard2.0;net7.0 + net452;netstandard2.0;net8.0 true false content true - true NU5128 diff --git a/MLEM.Templates/content/MLEM.Templates.DesktopGL/TemplateNamespace.csproj b/MLEM.Templates/content/MLEM.Templates.DesktopGL/TemplateNamespace.csproj index 41fa2bf..e4899ad 100644 --- a/MLEM.Templates/content/MLEM.Templates.DesktopGL/TemplateNamespace.csproj +++ b/MLEM.Templates/content/MLEM.Templates.DesktopGL/TemplateNamespace.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net8.0 false false Icon.ico @@ -19,9 +19,4 @@ - - - - - diff --git a/MLEM.Ui/Elements/Button.cs b/MLEM.Ui/Elements/Button.cs index a5c3425..bd74f49 100644 --- a/MLEM.Ui/Elements/Button.cs +++ b/MLEM.Ui/Elements/Button.cs @@ -87,6 +87,13 @@ namespace MLEM.Ui.Elements { private bool isDisabled; + /// + /// Creates a new button with the given settings and no text or tooltip. + /// + /// The button's anchor + /// The button's size + public Button(Anchor anchor, Vector2 size) : base(anchor, size) {} + /// /// Creates a new button with the given settings /// @@ -94,7 +101,7 @@ namespace MLEM.Ui.Elements { /// The button's size /// The text that should be displayed on the button /// The text that should be displayed in a when hovering over this button - public Button(Anchor anchor, Vector2 size, string text = null, string tooltipText = null) : base(anchor, size) { + public Button(Anchor anchor, Vector2 size, string text = null, string tooltipText = null) : this(anchor, size) { if (text != null) { this.Text = new Paragraph(Anchor.Center, 1, text, true); this.Text.Padding = this.Text.Padding.OrStyle(new Padding(1), 1); @@ -104,6 +111,23 @@ namespace MLEM.Ui.Elements { this.Tooltip = this.AddTooltip(tooltipText); } + /// + /// Creates a new button with the given settings + /// + /// The button's anchor + /// The button's size + /// The text that should be displayed on the button + /// The text that should be displayed in a when hovering over this button + public Button(Anchor anchor, Vector2 size, Paragraph.TextCallback textCallback = null, Paragraph.TextCallback tooltipTextCallback = null) : this(anchor, size) { + if (textCallback != null) { + this.Text = new Paragraph(Anchor.Center, 1, textCallback, true); + this.Text.Padding = this.Text.Padding.OrStyle(new Padding(1), 1); + this.AddChild(this.Text); + } + if (tooltipTextCallback != null) + this.Tooltip = this.AddTooltip(tooltipTextCallback); + } + /// public override void Draw(GameTime time, SpriteBatch batch, float alpha, SpriteBatchContext context) { var tex = this.Texture; diff --git a/MLEM.Ui/Elements/Dropdown.cs b/MLEM.Ui/Elements/Dropdown.cs index e9f080f..1131494 100644 --- a/MLEM.Ui/Elements/Dropdown.cs +++ b/MLEM.Ui/Elements/Dropdown.cs @@ -12,8 +12,7 @@ namespace MLEM.Ui.Elements { /// /// The panel that this dropdown contains. It will be displayed upon pressing the dropdown button. /// - public readonly Panel Panel; - + public Panel Panel { get; private set; } /// /// This property stores whether the dropdown is currently opened or not /// @@ -29,6 +28,18 @@ namespace MLEM.Ui.Elements { /// public GenericCallback OnOpenedOrClosed; + /// + /// Creates a new dropdown with the given settings and no text or tooltip. + /// + /// The dropdown's anchor + /// The dropdown button's size + /// The height of the . If this is 0, the panel will be set to . + /// Whether this dropdown's should automatically add a scroll bar to scroll towards elements that are beyond the area it covers. + /// Whether this dropdown's 's scroll bar should be hidden automatically if the panel does not contain enough children to allow for scrolling. This only has an effect if is . + public Dropdown(Anchor anchor, Vector2 size, float panelHeight = 0, bool scrollPanel = false, bool autoHidePanelScrollbar = true) : base(anchor, size) { + this.Initialize(panelHeight, scrollPanel, autoHidePanelScrollbar); + } + /// /// Creates a new dropdown with the given settings /// @@ -36,31 +47,25 @@ namespace MLEM.Ui.Elements { /// The dropdown button's size /// The text displayed on the dropdown button /// The text displayed as a tooltip when hovering over the dropdown button - public Dropdown(Anchor anchor, Vector2 size, string text = null, string tooltipText = null) : base(anchor, size, text, tooltipText) { - this.Panel = this.AddChild(new Panel(Anchor.TopCenter, Vector2.Zero, Vector2.Zero, true) { - IsHidden = true - }); - this.OnAreaUpdated += e => { - this.Panel.Size = new Vector2(e.Area.Width / e.Scale, 0); - this.Panel.PositionOffset = new Vector2(0, e.Area.Height / e.Scale); - }; - this.OnOpenedOrClosed += e => this.Priority = this.IsOpen ? 10000 : 0; - this.OnPressed += e => { - this.IsOpen = !this.IsOpen; - // close other dropdowns in the same root when we open - if (this.IsOpen) { - this.Root.Element.AndChildren(o => { - if (o != this && o is Dropdown d && d.IsOpen) - d.IsOpen = false; - }); - } - }; - this.GetGamepadNextElement = (dir, usualNext) => { - // Force navigate down to our first child if we're open - if (this.IsOpen && dir == Direction2.Down) - return this.Panel.GetChildren().FirstOrDefault(c => c.CanBeSelected) ?? usualNext; - return usualNext; - }; + /// The height of the . If this is 0, the panel will be set to . + /// Whether this dropdown's should automatically add a scroll bar to scroll towards elements that are beyond the area it covers. + /// Whether this dropdown's 's scroll bar should be hidden automatically if the panel does not contain enough children to allow for scrolling. This only has an effect if is . + public Dropdown(Anchor anchor, Vector2 size, string text = null, string tooltipText = null, float panelHeight = 0, bool scrollPanel = false, bool autoHidePanelScrollbar = true) : base(anchor, size, text, tooltipText) { + this.Initialize(panelHeight, scrollPanel, autoHidePanelScrollbar); + } + + /// + /// Creates a new dropdown with the given settings + /// + /// The dropdown's anchor + /// The dropdown button's size + /// The text displayed on the dropdown button + /// The text displayed as a tooltip when hovering over the dropdown button + /// The height of the . If this is 0, the panel will be set to . + /// Whether this dropdown's should automatically add a scroll bar to scroll towards elements that are beyond the area it covers. + /// Whether this dropdown's 's scroll bar should be hidden automatically if the panel does not contain enough children to allow for scrolling. This only has an effect if is . + public Dropdown(Anchor anchor, Vector2 size, Paragraph.TextCallback textCallback = null, Paragraph.TextCallback tooltipTextCallback = null, float panelHeight = 0, bool scrollPanel = false, bool autoHidePanelScrollbar = true) : base(anchor, size, textCallback, tooltipTextCallback) { + this.Initialize(panelHeight, scrollPanel, autoHidePanelScrollbar); } /// @@ -116,5 +121,32 @@ namespace MLEM.Ui.Elements { return paragraph; } + private void Initialize(float panelHeight, bool scrollPanel, bool autoHidePanelScrollbar) { + this.Panel = this.AddChild(new Panel(Anchor.TopCenter, Vector2.Zero, panelHeight == 0, scrollPanel, autoHidePanelScrollbar) { + IsHidden = true + }); + this.OnAreaUpdated += e => { + this.Panel.Size = new Vector2(e.Area.Width / e.Scale, panelHeight); + this.Panel.PositionOffset = new Vector2(0, e.Area.Height / e.Scale); + }; + this.OnOpenedOrClosed += e => this.Priority = this.IsOpen ? 10000 : 0; + this.OnPressed += e => { + this.IsOpen = !this.IsOpen; + // close other dropdowns in the same root when we open + if (this.IsOpen) { + this.Root.Element.AndChildren(o => { + if (o != this && o is Dropdown d && d.IsOpen) + d.IsOpen = false; + }); + } + }; + this.GetGamepadNextElement = (dir, usualNext) => { + // Force navigate down to our first child if we're open + if (this.IsOpen && dir == Direction2.Down) + return this.Panel.GetChildren().FirstOrDefault(c => c.CanBeSelected) ?? usualNext; + return usualNext; + }; + } + } } diff --git a/MLEM.Ui/Elements/Element.cs b/MLEM.Ui/Elements/Element.cs index 3daa80d..45eb7cf 100644 --- a/MLEM.Ui/Elements/Element.cs +++ b/MLEM.Ui/Elements/Element.cs @@ -1236,12 +1236,13 @@ namespace MLEM.Ui.Elements { /// public override string ToString() { - var ret = this.GetType().ToString(); - // elements will contain their path up to the root (Paragraph@Panel@...@RootName) + var ret = this.GetType().Name; + // elements will contain their path up to the root and their index in each parent + // eg Paragraph 2 @ Panel 3 @ ... @ Group RootName if (this.Parent != null) { - ret += $"@{this.Parent}"; + ret += $" {this.Parent.Children.IndexOf(this)} @ {this.Parent}"; } else if (this.Root?.Element == this) { - ret += $"@{this.Root.Name}"; + ret += $" {this.Root.Name}"; } return ret; } diff --git a/MLEM.Ui/Elements/Group.cs b/MLEM.Ui/Elements/Group.cs index de6f1a9..ca3a9ad 100644 --- a/MLEM.Ui/Elements/Group.cs +++ b/MLEM.Ui/Elements/Group.cs @@ -14,8 +14,18 @@ namespace MLEM.Ui.Elements { /// /// The group's anchor /// The group's size - /// Whether the group's height should be based on its children's height - public Group(Anchor anchor, Vector2 size, bool setHeightBasedOnChildren = true) : base(anchor, size) { + /// Whether the group's height should be based on its children's height, see . + public Group(Anchor anchor, Vector2 size, bool setHeightBasedOnChildren = true) : this(anchor, size, false, setHeightBasedOnChildren) {} + + /// + /// Creates a new group with the given settings + /// + /// The group's anchor + /// The group's size + /// Whether the group's width should be based on its children's width, see . + /// Whether the group's height should be based on its children's height, see . + public Group(Anchor anchor, Vector2 size, bool setWidthBasedOnChildren, bool setHeightBasedOnChildren) : base(anchor, size) { + this.SetWidthBasedOnChildren = setWidthBasedOnChildren; this.SetHeightBasedOnChildren = setHeightBasedOnChildren; this.CanBeSelected = false; } diff --git a/MLEM.Ui/Elements/Image.cs b/MLEM.Ui/Elements/Image.cs index 00be592..26411aa 100644 --- a/MLEM.Ui/Elements/Image.cs +++ b/MLEM.Ui/Elements/Image.cs @@ -69,11 +69,45 @@ namespace MLEM.Ui.Elements { /// Note that increased rotation does not increase this component's size, even if the rotated texture would go out of bounds of this component. /// public float ImageRotation; + /// + /// Whether this image's width should automatically be calculated based on this image's calculated height in relation to its 's aspect ratio. + /// Note that, if this is , the value will still be applied to this image's width. + /// + public bool SetWidthBasedOnAspect { + get => this.setWidthBasedOnAspect; + set { + if (this.setWidthBasedOnAspect != value) { + this.setWidthBasedOnAspect = value; + this.SetAreaDirty(); + } + } + } + /// + /// Whether this image's height should automatically be calculated based on this image's calculated width in relation to its 's aspect ratio. + /// This behavior is useful if an image should take up a certain width, but the aspect ratio of its texture can vary and the image should not take up more height than is necessary. + /// Note that, if this is , the value will still be applied to this image's height. + /// + public bool SetHeightBasedOnAspect { + get => this.setHeightBasedOnAspect; + set { + if (this.setHeightBasedOnAspect != value) { + this.setHeightBasedOnAspect = value; + this.SetAreaDirty(); + } + } + } + /// + /// The sampler state that this image's should be drawn with. + /// If this is , the current 's will be used, which will likely be the same as . + /// + public SamplerState SamplerState; /// public override bool IsHidden => base.IsHidden || this.Texture == null; private bool scaleToImage; + private bool setWidthBasedOnAspect; + private bool setHeightBasedOnAspect; private TextureRegion explicitlySetTexture; private TextureRegion displayedTexture; @@ -86,7 +120,7 @@ namespace MLEM.Ui.Elements { /// Whether this image's size should be based on the texture's size public Image(Anchor anchor, Vector2 size, TextureRegion texture, bool scaleToImage = false) : base(anchor, size) { this.Texture = texture; - this.scaleToImage = scaleToImage; + this.ScaleToImage = scaleToImage; this.CanBeSelected = false; this.CanBeMoused = false; } @@ -95,14 +129,29 @@ namespace MLEM.Ui.Elements { public Image(Anchor anchor, Vector2 size, TextureCallback getTextureCallback, bool scaleToImage = false) : base(anchor, size) { this.GetTextureCallback = getTextureCallback; this.Texture = getTextureCallback(this); - this.scaleToImage = scaleToImage; + this.ScaleToImage = scaleToImage; this.CanBeSelected = false; this.CanBeMoused = false; } /// protected override Vector2 CalcActualSize(RectangleF parentArea) { - return this.Texture != null && this.scaleToImage ? this.Texture.Size.ToVector2() * this.Scale : base.CalcActualSize(parentArea); + var ret = base.CalcActualSize(parentArea); + if (this.Texture != null) { + if (this.ScaleToImage) + ret = this.Texture.Size.ToVector2() * this.Scale; + if (this.SetWidthBasedOnAspect) + ret.X = ret.Y * this.Texture.Width / this.Texture.Height + this.ScaledAutoSizeAddedAbsolute.X; + if (this.SetHeightBasedOnAspect) + ret.Y = ret.X * this.Texture.Height / this.Texture.Width + this.ScaledAutoSizeAddedAbsolute.Y; + } else { + // if we don't have a texture and we auto-set width or height, calculate as if we had a texture with a size of 0 + if (this.SetWidthBasedOnAspect) + ret.X = this.ScaledAutoSizeAddedAbsolute.X; + if (this.SetHeightBasedOnAspect) + ret.Y = this.ScaledAutoSizeAddedAbsolute.Y; + } + return ret; } /// @@ -115,6 +164,14 @@ namespace MLEM.Ui.Elements { public override void Draw(GameTime time, SpriteBatch batch, float alpha, SpriteBatchContext context) { if (this.Texture == null) return; + + if (this.SamplerState != null) { + batch.End(); + var localContext = context; + localContext.SamplerState = this.SamplerState; + batch.Begin(localContext); + } + var center = new Vector2(this.Texture.Width / 2F, this.Texture.Height / 2F); var color = this.Color.OrDefault(Microsoft.Xna.Framework.Color.White) * alpha; if (this.MaintainImageAspect) { @@ -125,6 +182,12 @@ namespace MLEM.Ui.Elements { var scale = new Vector2(1F / this.Texture.Width, 1F / this.Texture.Height) * this.DisplayArea.Size; batch.Draw(this.Texture, this.DisplayArea.Location + center * scale, color, this.ImageRotation, center, scale * this.ImageScale, this.ImageEffects, 0); } + + if (this.SamplerState != null) { + batch.End(); + batch.Begin(context); + } + base.Draw(time, batch, alpha, context); } @@ -134,7 +197,7 @@ namespace MLEM.Ui.Elements { return; var nullChanged = this.displayedTexture == null != (newTexture == null); this.displayedTexture = newTexture; - if (nullChanged || this.scaleToImage) + if (nullChanged || this.ScaleToImage || this.SetWidthBasedOnAspect || this.SetHeightBasedOnAspect) this.SetAreaDirty(); } diff --git a/MLEM.Ui/Elements/Panel.cs b/MLEM.Ui/Elements/Panel.cs index 7ae328c..2f1dc9d 100644 --- a/MLEM.Ui/Elements/Panel.cs +++ b/MLEM.Ui/Elements/Panel.cs @@ -55,12 +55,16 @@ namespace MLEM.Ui.Elements { } private readonly List relevantChildren = new List(); + private readonly HashSet scrolledChildren = new HashSet(); + private readonly float[] scrollBarMaxHistory; private readonly bool scrollOverflow; private RenderTarget2D renderTarget; private bool relevantChildrenDirty; private float scrollBarChildOffset; private StyleProp scrollBarOffset; + private float lastScrollOffset; + private bool childrenDirtyForScroll; /// /// Creates a new panel with the given settings. @@ -70,7 +74,7 @@ namespace MLEM.Ui.Elements { /// The panel's offset from its anchor point /// Whether the panel should automatically calculate its height based on its children's size /// Whether this panel should automatically add a scroll bar to scroll towards elements that are beyond the area this panel covers - /// Whether the scroll bar should be hidden automatically if the panel does not contain enough children to allow for scrolling + /// Whether the scroll bar should be hidden automatically if the panel does not contain enough children to allow for scrolling. This only has an effect if is . public Panel(Anchor anchor, Vector2 size, Vector2 positionOffset, bool setHeightBasedOnChildren = false, bool scrollOverflow = false, bool autoHideScrollbar = true) : base(anchor, size) { this.PositionOffset = positionOffset; this.SetHeightBasedOnChildren = setHeightBasedOnChildren; @@ -94,9 +98,23 @@ namespace MLEM.Ui.Elements { this.ScrollToElement(e); }; this.AddChild(this.ScrollBar); + + this.scrollBarMaxHistory = new float[3]; + for (var i = 0; i < this.scrollBarMaxHistory.Length; i++) + this.scrollBarMaxHistory[i] = -1; } } + /// + /// Creates a new panel with the given settings. + /// + /// The panel's anchor + /// The panel's default size + /// Whether the panel should automatically calculate its height based on its children's size + /// Whether this panel should automatically add a scroll bar to scroll towards elements that are beyond the area this panel covers + /// Whether the scroll bar should be hidden automatically if the panel does not contain enough children to allow for scrolling. This only has an effect if is . + public Panel(Anchor anchor, Vector2 size, bool setHeightBasedOnChildren = false, bool scrollOverflow = false, bool autoHideScrollbar = true) : this(anchor, size, Vector2.Zero, setHeightBasedOnChildren, scrollOverflow, autoHideScrollbar) {} + /// public override void ForceUpdateArea() { if (this.scrollOverflow) { @@ -106,11 +124,15 @@ namespace MLEM.Ui.Elements { foreach (var child in this.Children) { if (child != this.ScrollBar && !child.Anchor.IsAuto()) throw new NotSupportedException($"A panel that handles overflow can't contain non-automatic anchors ({child})"); - if (child is Panel panel && panel.scrollOverflow) - throw new NotSupportedException($"A panel that scrolls overflow cannot contain another panel that scrolls overflow ({child})"); } } + base.ForceUpdateArea(); + if (this.scrollOverflow) { + for (var i = 0; i < this.scrollBarMaxHistory.Length; i++) + this.scrollBarMaxHistory[i] = -1; + } + this.SetScrollBarStyle(); } @@ -136,8 +158,16 @@ namespace MLEM.Ui.Elements { // when removing children, our scroll bar might have to be hidden // if we don't do this before adding children again, they might incorrectly assume that the scroll bar will still be visible and adjust their size accordingly - if (this.System != null) + this.childrenDirtyForScroll = true; + } + + /// + public override T AddChild(T element, int index = -1) { + // if children were recently removed, make sure to update the scroll bar before adding new ones so that they can't incorrectly assume the scroll bar will be visible + if (this.childrenDirtyForScroll && this.System != null) this.ScrollSetup(); + + return base.AddChild(element, index); } /// @@ -202,10 +232,10 @@ namespace MLEM.Ui.Elements { /// /// 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) + var highestValidChild = this.Children.FirstOrDefault(c => c != this.ScrollBar && !c.IsHidden); + if (highestValidChild == null) return; - this.ScrollBar.CurrentValue = (elementY - this.Area.Height / 2 - firstChild.Area.Top) / this.Scale + this.ChildPadding.Value.Height / 2; + this.ScrollBar.CurrentValue = (elementY - this.Area.Height / 2 - highestValidChild.Area.Top) / this.Scale + this.ChildPadding.Value.Height / 2; } /// @@ -234,13 +264,12 @@ namespace MLEM.Ui.Elements { /// 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) { + if (grandchild && !this.AreaDirty) { + // 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 this.ScrollChildren(); // we also need to re-setup here in case the child is involved in a special GetTotalCoveredArea - if (!this.AreaDirty) - this.ScrollSetup(); + this.ScrollSetup(); } } @@ -257,32 +286,41 @@ namespace MLEM.Ui.Elements { /// Prepares the panel for auto-scrolling, creating the render target and setting up the scroll bar's maximum value. /// protected virtual void ScrollSetup() { + this.childrenDirtyForScroll = false; + if (!this.scrollOverflow || this.IsHidden) return; float childrenHeight; if (this.Children.Count > 1) { - var firstChild = this.Children.FirstOrDefault(c => c != this.ScrollBar); + var highestValidChild = this.Children.FirstOrDefault(c => c != this.ScrollBar && !c.IsHidden); var lowestChild = this.GetLowestChild(c => c != this.ScrollBar && !c.IsHidden, true); - childrenHeight = lowestChild.GetTotalCoveredArea(false).Bottom - firstChild.Area.Top; + childrenHeight = lowestChild.GetTotalCoveredArea(false).Bottom - highestValidChild.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 scroll bar is the amount of non-scaled pixels taken up by overflowing components - var scrollBarMax = (childrenHeight - this.ChildPaddedArea.Height) / this.Scale; + var scrollBarMax = Math.Max(0, (childrenHeight - this.ChildPaddedArea.Height) / this.Scale); if (!this.ScrollBar.MaxValue.Equals(scrollBarMax, Element.Epsilon)) { - this.ScrollBar.MaxValue = scrollBarMax; - this.relevantChildrenDirty = true; + // avoid a show/hide oscillation that occurs while updating our area with children that can lose height when the scroll bar is shown (like long paragraphs) + if (!this.scrollBarMaxHistory[0].Equals(this.scrollBarMaxHistory[2], Element.Epsilon) || !this.scrollBarMaxHistory[1].Equals(scrollBarMax, Element.Epsilon)) { + this.scrollBarMaxHistory[0] = this.scrollBarMaxHistory[1]; + this.scrollBarMaxHistory[1] = this.scrollBarMaxHistory[2]; + this.scrollBarMaxHistory[2] = scrollBarMax; + + 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); + var childOffsetDelta = childOffset - this.scrollBarChildOffset; + if (!childOffsetDelta.Equals(0, Element.Epsilon)) { this.scrollBarChildOffset = childOffset; - this.SetAreaDirty(); + this.ChildPadding += new Padding(0, childOffsetDelta, 0, 0); } // 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 @@ -290,15 +328,15 @@ namespace MLEM.Ui.Elements { this.ScrollBar.ScrollerSize = new Vector2(this.ScrollerSize.Value.X, Math.Max(this.ScrollerSize.Value.Y, scrollerHeight)); // update the render target - var targetArea = (Rectangle) this.GetRenderTargetArea(); - if (targetArea.Width <= 0 || targetArea.Height <= 0) { + var area = (Rectangle) this.GetRenderTargetArea(); + if (area.Width <= 0 || area.Height <= 0) { this.renderTarget?.Dispose(); this.renderTarget = null; return; } - if (this.renderTarget == null || targetArea.Width != this.renderTarget.Width || targetArea.Height != this.renderTarget.Height) { + if (this.renderTarget == null || area.Width != this.renderTarget.Width || area.Height != this.renderTarget.Height) { this.renderTarget?.Dispose(); - this.renderTarget = targetArea.IsEmpty ? null : new RenderTarget2D(this.System.Game.GraphicsDevice, targetArea.Width, targetArea.Height); + this.renderTarget = new RenderTarget2D(this.System.Game.GraphicsDevice, area.Width, area.Height, false, SurfaceFormat.Color, DepthFormat.None, 0, RenderTargetUsage.PreserveContents); this.relevantChildrenDirty = true; } } @@ -330,7 +368,7 @@ namespace MLEM.Ui.Elements { } private RectangleF GetRenderTargetArea() { - var area = this.ChildPaddedArea; + var area = this.ChildPaddedArea.OffsetCopy(this.ScaledScrollOffset); area.X = this.DisplayArea.X; area.Width = this.DisplayArea.Width; return area; @@ -339,9 +377,21 @@ namespace MLEM.Ui.Elements { private void ScrollChildren() { if (!this.scrollOverflow) return; + + var currentChildren = new HashSet(); + // scroll all our children (and cache newly added ones) // 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; + foreach (var child in this.GetChildren(c => c != this.ScrollBar, true, true)) { + // if a child was newly added later, the last scroll offset was never applied + if (this.scrolledChildren.Add(child)) + child.ScrollOffset.Y -= this.lastScrollOffset; + child.ScrollOffset.Y += (this.lastScrollOffset - this.ScrollBar.CurrentValue); + currentChildren.Add(child); + } + // remove cached scrolled children that aren't our children anymore + this.scrolledChildren.IntersectWith(currentChildren); + + this.lastScrollOffset = this.ScrollBar.CurrentValue; this.relevantChildrenDirty = true; } diff --git a/MLEM.Ui/Elements/Paragraph.cs b/MLEM.Ui/Elements/Paragraph.cs index a6950f8..3560f56 100644 --- a/MLEM.Ui/Elements/Paragraph.cs +++ b/MLEM.Ui/Elements/Paragraph.cs @@ -166,6 +166,30 @@ namespace MLEM.Ui.Elements { private float textScaleMultiplier = 1; private bool autoAdjustWidth; + /// + /// Creates a new paragraph with the given settings. + /// + /// The paragraph's anchor + /// The paragraph's width. Note that its height is automatically calculated. + /// The paragraph's text + /// The paragraph's text alignment. + /// Whether the paragraph's width should automatically be calculated based on the text within it. + public Paragraph(Anchor anchor, float width, TextCallback textCallback, TextAlignment alignment, bool autoAdjustWidth = false) : this(anchor, width, textCallback, autoAdjustWidth) { + this.Alignment = alignment; + } + + /// + /// Creates a new paragraph with the given settings. + /// + /// The paragraph's anchor + /// The paragraph's width. Note that its height is automatically calculated. + /// The paragraph's text + /// The paragraph's text alignment. + /// Whether the paragraph's width should automatically be calculated based on the text within it. + public Paragraph(Anchor anchor, float width, string text, TextAlignment alignment, bool autoAdjustWidth = false) : this(anchor, width, text, autoAdjustWidth) { + this.Alignment = alignment; + } + /// /// Creates a new paragraph with the given settings. /// @@ -177,7 +201,13 @@ namespace MLEM.Ui.Elements { this.GetTextCallback = textCallback; } - /// + /// + /// Creates a new paragraph with the given settings. + /// + /// The paragraph's anchor + /// The paragraph's width. Note that its height is automatically calculated. + /// The paragraph's text + /// Whether the paragraph's width should automatically be calculated based on the text within it. public Paragraph(Anchor anchor, float width, string text, bool autoAdjustWidth = false) : base(anchor, new Vector2(width, 0)) { this.Text = text; this.AutoAdjustWidth = autoAdjustWidth; @@ -232,7 +262,7 @@ namespace MLEM.Ui.Elements { private void SetTextDirty() { this.tokenizedText = null; // only set our area dirty if our size changed as a result of this action - if (!this.AreaDirty && !this.CalcActualSize(this.ParentArea).Equals(this.DisplayArea.Size, Element.Epsilon)) + if (!this.AreaDirty && (this.System == null || !this.CalcActualSize(this.ParentArea).Equals(this.DisplayArea.Size, Element.Epsilon))) this.SetAreaDirty(); } diff --git a/MLEM.Ui/Elements/ScrollBar.cs b/MLEM.Ui/Elements/ScrollBar.cs index 6fce5bc..0b2e2d7 100644 --- a/MLEM.Ui/Elements/ScrollBar.cs +++ b/MLEM.Ui/Elements/ScrollBar.cs @@ -158,7 +158,7 @@ namespace MLEM.Ui.Elements { if (this.isMouseScrolling) this.ScrollToPos(this.TransformInverseAll(this.Input.ViewportMousePosition.ToVector2())); if (!this.Horizontal) { - if (moused != null && (moused == this.Parent || moused.GetParentTree().Contains(this.Parent))) { + if (this.IsMousedForScrolling(moused)) { var scroll = this.Input.LastScrollWheel - this.Input.ScrollWheel; if (scroll != 0) this.CurrentValue += this.StepPerScroll * Math.Sign(scroll); @@ -244,6 +244,23 @@ namespace MLEM.Ui.Elements { this.SmoothScrollFactor = this.SmoothScrollFactor.OrStyle(style.ScrollBarSmoothScrollFactor); } + private bool IsMousedForScrolling(Element moused) { + if (moused == null || (moused != this.Parent && !moused.GetParentTree().Contains(this.Parent))) + return false; + // if we're moused, check if there are any scroll bars deeper than us that should take precedence + var foundMe = false; + foreach (var child in this.Parent.GetChildren(regardGrandchildren: true)) { + if (foundMe) { + if (child is ScrollBar b && !b.Horizontal && b.IsMousedForScrolling(moused)) + return false; + } else if (child == this) { + // once we found ourselves, all subsequent children are deeper/older! + foundMe = true; + } + } + return true; + } + /// /// A delegate method used for /// diff --git a/MLEM.Ui/Elements/TextField.cs b/MLEM.Ui/Elements/TextField.cs index e7cbb46..998e3cd 100644 --- a/MLEM.Ui/Elements/TextField.cs +++ b/MLEM.Ui/Elements/TextField.cs @@ -204,8 +204,6 @@ namespace MLEM.Ui.Elements { if (this.IsSelectedActive && !this.IsHidden && !this.textInput.OnTextInput(key, character) && key == Keys.Enter && !this.Multiline) this.EnterReceiver?.Controls?.PressElement(this.EnterReceiver); }; - this.OnDeselected += e => this.CaretPos = 0; - this.OnSelected += e => this.CaretPos = this.textInput.Length; } /// diff --git a/MLEM.Ui/MLEM.Ui.FNA.csproj b/MLEM.Ui/MLEM.Ui.FNA.csproj index 34c8c01..22d4978 100644 --- a/MLEM.Ui/MLEM.Ui.FNA.csproj +++ b/MLEM.Ui/MLEM.Ui.FNA.csproj @@ -1,9 +1,9 @@  - net452;netstandard2.0;net7.0 + net452;netstandard2.0;net8.0 true true - true + true MLEM.Ui $(DefineConstants);FNA @@ -24,7 +24,7 @@ - + all diff --git a/MLEM.Ui/MLEM.Ui.csproj b/MLEM.Ui/MLEM.Ui.csproj index 6da4337..c01bb34 100644 --- a/MLEM.Ui/MLEM.Ui.csproj +++ b/MLEM.Ui/MLEM.Ui.csproj @@ -1,9 +1,9 @@  - net452;netstandard2.0;net7.0 + net452;netstandard2.0;net8.0 true true - true + true diff --git a/MLEM.Ui/Parsers/UiParser.cs b/MLEM.Ui/Parsers/UiParser.cs index b1c3b50..f6f8e78 100644 --- a/MLEM.Ui/Parsers/UiParser.cs +++ b/MLEM.Ui/Parsers/UiParser.cs @@ -139,21 +139,32 @@ namespace MLEM.Ui.Parsers { /// This method invokes an asynchronouns action, meaning the 's will likely not have loaded in when this method returns. /// /// The absolute, relative or web path to the image. + /// An action that is invoked with the loaded image once it is fetched. Note that this action will be invoked asynchronously. /// The loaded image. /// Thrown if is null, or if there is an loading the image and is unset. - protected Image ParseImage(string path) { + protected Image ParseImage(string path, Action onImageFetched = null) { if (this.GraphicsDevice == null) throw new NullReferenceException("A UI parser requires a GraphicsDevice for parsing images"); + var imageLock = new object(); TextureRegion image = null; - return new Image(Anchor.AutoLeft, new Vector2(1, -1), _ => image) { + return new Image(Anchor.AutoLeft, Vector2.One, _ => { + lock (imageLock) + return image; + }) { + SetHeightBasedOnAspect = true, OnAddedToUi = e => { - if (image == null) + bool imageNull; + lock (imageLock) + imageNull = image == null; + if (imageNull) LoadImageAsync(); }, OnRemovedFromUi = e => { - image?.Texture.Dispose(); - image = null; + lock (imageLock) { + image?.Texture.Dispose(); + image = null; + } } }; @@ -178,7 +189,12 @@ namespace MLEM.Ui.Parsers { using (var stream = Path.IsPathRooted(path) ? File.OpenRead(path) : TitleContainer.OpenStream(path)) tex = Texture2D.FromStream(this.GraphicsDevice, stream); } - image = new TextureRegion(tex); + lock (imageLock) { + if (image == null) { + image = new TextureRegion(tex); + onImageFetched?.Invoke(image); + } + } } catch (Exception e) { if (this.ImageExceptionHandler != null) { this.ImageExceptionHandler.Invoke(path, e); diff --git a/MLEM.Ui/UiControls.cs b/MLEM.Ui/UiControls.cs index ec69172..a8d35ad 100644 --- a/MLEM.Ui/UiControls.cs +++ b/MLEM.Ui/UiControls.cs @@ -103,7 +103,7 @@ namespace MLEM.Ui { /// public bool HandleGamepad = true; /// - /// If this value is true, the ui controls are in automatic navigation mode. + /// If this value is true, the ui controls are in automatic navigation mode. The state of automatic navigation is usually based on the current . /// This means that the will be drawn around the . /// public bool IsAutoNavMode { @@ -115,12 +115,29 @@ namespace MLEM.Ui { } } } + /// + /// The current of these ui controls, which represents the last type of interaction that was used to interact with the underlying . + /// + public NavigationType NavType { + get => this.navType; + set { + if (this.navType != value) { + var last = this.navType; + this.navType = value; + this.NavTypeChanged?.Invoke(last, value); + } + } + } /// /// An event that is raised when is changed. /// This can be used for custom actions like hiding the mouse cursor when automatic navigation is enabled. /// public event Action AutoNavModeChanged; + /// + /// An event that is raised when is changed. It receives the previous navigation type, as well as the newly set navigation type. + /// + public event Action NavTypeChanged; /// /// This value ist true if the was created by this ui controls instance, or if it was passed in. @@ -134,6 +151,7 @@ namespace MLEM.Ui { private readonly Dictionary selectedElements = new Dictionary(); private bool isAutoNavMode; + private NavigationType navType; /// /// Creates a new instance of the ui controls. @@ -167,6 +185,7 @@ namespace MLEM.Ui { if (this.Input.IsPressedAvailable(MouseButton.Left)) { this.IsAutoNavMode = false; + this.NavType = NavigationType.Mouse; var selectedNow = mousedNow != null && mousedNow.CanBeSelected ? mousedNow : null; this.SelectElement(this.ActiveRoot, selectedNow); if (mousedNow != null && mousedNow.CanBePressed) { @@ -175,6 +194,7 @@ namespace MLEM.Ui { } } else if (this.Input.IsPressedAvailable(MouseButton.Right)) { this.IsAutoNavMode = false; + this.NavType = NavigationType.Mouse; if (mousedNow != null && mousedNow.CanBePressed) { this.PressElement(mousedNow, true); this.Input.TryConsumePressed(MouseButton.Right); @@ -187,12 +207,14 @@ namespace MLEM.Ui { if (this.HandleKeyboard) { if (this.KeyboardButtons.IsPressedAvailable(this.Input, this.GamepadIndex)) { if (this.SelectedElement?.Root != null && this.SelectedElement.CanBePressed) { + this.NavType = NavigationType.Keyboard; // primary or secondary action on element using space or enter this.PressElement(this.SelectedElement, this.Input.IsModifierKeyDown(ModifierKey.Shift)); this.KeyboardButtons.TryConsumePressed(this.Input, this.GamepadIndex); } } else if (this.Input.IsPressedAvailable(Keys.Tab)) { this.IsAutoNavMode = true; + this.NavType = NavigationType.Keyboard; // tab or shift-tab to next or previous element var backward = this.Input.IsModifierKeyDown(ModifierKey.Shift); var next = this.GetTabNextElement(backward); @@ -208,12 +230,14 @@ namespace MLEM.Ui { if (this.HandleTouch) { if (this.Input.GetViewportGesture(GestureType.Tap, out var tap)) { this.IsAutoNavMode = false; + this.NavType = NavigationType.Touch; var tapped = this.GetElementUnderPos(tap.Position); this.SelectElement(this.ActiveRoot, tapped); if (tapped != null && tapped.CanBePressed) this.PressElement(tapped); } else if (this.Input.GetViewportGesture(GestureType.Hold, out var hold)) { this.IsAutoNavMode = false; + this.NavType = NavigationType.Touch; var held = this.GetElementUnderPos(hold.Position); this.SelectElement(this.ActiveRoot, held); if (held != null && held.CanBePressed) @@ -224,6 +248,7 @@ namespace MLEM.Ui { foreach (var location in this.Input.ViewportTouchState) { var element = this.GetElementUnderPos(location.Position); if (location.State == TouchLocationState.Pressed) { + this.NavType = NavigationType.Touch; // start touching an element if we just touched down on it this.SetTouchedElement(element); } else if (element != this.TouchedElement) { @@ -239,11 +264,13 @@ namespace MLEM.Ui { if (this.HandleGamepad) { if (this.GamepadButtons.IsPressedAvailable(this.Input, this.GamepadIndex)) { if (this.SelectedElement?.Root != null && this.SelectedElement.CanBePressed) { + this.NavType = NavigationType.Gamepad; this.PressElement(this.SelectedElement); this.GamepadButtons.TryConsumePressed(this.Input, this.GamepadIndex); } } else if (this.SecondaryGamepadButtons.IsPressedAvailable(this.Input, this.GamepadIndex)) { if (this.SelectedElement?.Root != null && this.SelectedElement.CanBePressed) { + this.NavType = NavigationType.Gamepad; this.PressElement(this.SelectedElement, true); this.SecondaryGamepadButtons.TryConsumePressed(this.Input, this.GamepadIndex); } @@ -286,8 +313,9 @@ namespace MLEM.Ui { /// /// The root element of the /// The element to select, or null to deselect the selected element. - /// Whether automatic navigation should be forced on - public void SelectElement(RootElement root, Element element, bool? autoNav = null) { + /// Whether automatic navigation should be forced on. If this is , the automatic navigation state will stay the same. + /// An optional to set. If this is , the navigation type will stay the same. + public void SelectElement(RootElement root, Element element, bool? autoNav = null, NavigationType? navType = null) { if (root == null) return; if (element != null && !element.CanBeSelected) @@ -308,6 +336,8 @@ namespace MLEM.Ui { if (autoNav != null) this.IsAutoNavMode = autoNav.Value; + if (navType != null) + this.NavType = navType.Value; } /// @@ -444,6 +474,7 @@ namespace MLEM.Ui { private bool HandleGamepadNextElement(Direction2 dir) { this.IsAutoNavMode = true; + this.NavType = NavigationType.Gamepad; var next = this.GetGamepadNextElement(dir); if (this.SelectedElement != null) next = this.SelectedElement.GetGamepadNextElement(dir, next); @@ -454,5 +485,34 @@ namespace MLEM.Ui { return false; } + /// + /// An enumeration type that represents the possible types of navigation that a instance supports. + /// This is used by , which stores the most recently used navigation type for a . + /// + public enum NavigationType { + + /// + /// An unknown navigation type, which usually means there has not been any ui navigation of any type yet. + /// + Unknown = 0, + /// + /// Mouse cursor and mouse button navigation. + /// + Mouse, + /// + /// Keyboard navigation. + /// + Keyboard, + /// + /// Touch and gesture navigation. + /// + Touch, + /// + /// Gamepad-style navigation, which may also include arrow key-based navigation based on current settings. + /// + Gamepad + + } + } } diff --git a/MLEM.Ui/UiSystem.cs b/MLEM.Ui/UiSystem.cs index a96c4f2..2e80d72 100644 --- a/MLEM.Ui/UiSystem.cs +++ b/MLEM.Ui/UiSystem.cs @@ -608,9 +608,10 @@ namespace MLEM.Ui { /// Optionally, automatic navigation can be forced on, causing the to be drawn around the element. /// /// The element to select, or null to deselect the selected element. - /// Whether automatic navigation should be forced on - public void SelectElement(Element element, bool? autoNav = null) { - this.System.Controls.SelectElement(this, element, autoNav); + /// Whether automatic navigation should be forced on. If this is , the automatic navigation state will stay the same. + /// An optional to set. If this is , the navigation type will stay the same. + public void SelectElement(Element element, bool? autoNav = null, UiControls.NavigationType? navType = null) { + this.System.Controls.SelectElement(this, element, autoNav, navType); } /// diff --git a/MLEM/Extensions/ColorExtensions.cs b/MLEM/Extensions/ColorExtensions.cs index 05b66f2..0ceebe0 100644 --- a/MLEM/Extensions/ColorExtensions.cs +++ b/MLEM/Extensions/ColorExtensions.cs @@ -1,3 +1,4 @@ +using System; using System.Globalization; using Microsoft.Xna.Framework; @@ -36,6 +37,27 @@ namespace MLEM.Extensions { return new Color(color.ToVector4() * other.ToVector4()); } + /// + /// Returns the hexadecimal representation of this color as a string in the format #AARRGGBB, or optionally AARRGGBB, without the pound symbol. + /// + /// The color to convert. + /// Whether a # should prepend the string. + /// The resulting hex string. + public static string ToHexStringRgba(this Color color, bool hash = true) { + return $"{(hash ? "#" : string.Empty)}{color.A:X2}{color.R:X2}{color.G:X2}{color.B:X2}"; + } + + /// + /// Returns the hexadecimal representation of this color as a string in the format #RRGGBB, or optionally RRGGBB, without the pound symbol. + /// The alpha channel is ignored. + /// + /// The color to convert. + /// Whether a # should prepend the string. + /// The resulting hex string. + public static string ToHexStringRgb(this Color color, bool hash = true) { + return $"{(hash ? "#" : string.Empty)}{color.R:X2}{color.G:X2}{color.B:X2}"; + } + } /// @@ -64,16 +86,34 @@ namespace MLEM.Extensions { } /// - /// Parses a hexadecimal string into a color. - /// The string can either be formatted as RRGGBB or AARRGGBB and can optionally start with a #. + /// Parses a hexadecimal string into a color and throws a if parsing fails. + /// The string can either be formatted as RRGGBB or AARRGGBB and can optionally start with a #. /// /// The string to parse. /// The resulting color. + /// Thrown if parsing fails. public static Color FromHexString(string value) { + if (!ColorHelper.TryFromHexString(value, out var val)) + throw new FormatException($"Cannot parse hex string {value}"); + return val; + } + + /// + /// Tries to parse a hexadecimal string into a color and returns whether a color was successfully parsed. + /// The string can either be formatted as RRGGBB or AARRGGBB and can optionally start with a #. + /// + /// The string to parse. + /// The resulting color. + /// Whether parsing was successful. + public static bool TryFromHexString(string value, out Color color) { if (value.StartsWith("#")) value = value.Substring(1); - var val = int.Parse(value, NumberStyles.HexNumber); - return value.Length > 6 ? ColorHelper.FromHexRgba(val) : ColorHelper.FromHexRgb(val); + if (int.TryParse(value, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var val)) { + color = value.Length > 6 ? ColorHelper.FromHexRgba(val) : ColorHelper.FromHexRgb(val); + return true; + } + color = default; + return false; } } diff --git a/MLEM/Extensions/GraphicsExtensions.cs b/MLEM/Extensions/GraphicsExtensions.cs index 4c1b230..57dd9b8 100644 --- a/MLEM/Extensions/GraphicsExtensions.cs +++ b/MLEM/Extensions/GraphicsExtensions.cs @@ -69,11 +69,22 @@ namespace MLEM.Extensions { /// /// The graphics device /// The render target to apply - /// + /// The render target context, to be used in a using statement public static TargetContext WithRenderTarget(this GraphicsDevice device, RenderTarget2D target) { return new TargetContext(device, target); } + /// + /// Starts a new using the specified render target bindings. + /// The returned context automatically disposes when used in a using statement, which causes any previously applied render targets to be reapplied automatically. + /// + /// The graphics device + /// The render targets to apply + /// The render target context, to be used in a using statement + public static TargetContext WithRenderTargets(this GraphicsDevice device, params RenderTargetBinding[] targets) { + return new TargetContext(device, targets); + } + /// /// Represents a context in which a is applied. /// This class should be used with . @@ -88,7 +99,20 @@ namespace MLEM.Extensions { /// /// The graphics device to apply the target on /// The target to apply - public TargetContext(GraphicsDevice device, RenderTarget2D target) { + public TargetContext(GraphicsDevice device, RenderTarget2D target) : this(device) { + device.SetRenderTarget(target); + } + + /// + /// Creates a new target context with the given settings. + /// + /// The graphics device to apply the target on + /// The targets to apply + public TargetContext(GraphicsDevice device, RenderTargetBinding[] targets) : this(device) { + device.SetRenderTargets(targets); + } + + private TargetContext(GraphicsDevice device) { this.device = device; #if FNA // RenderTargetCount doesn't exist in FNA but we still want the optimization in MG @@ -96,7 +120,6 @@ namespace MLEM.Extensions { #else this.lastTargets = device.RenderTargetCount <= 0 ? null : device.GetRenderTargets(); #endif - device.SetRenderTarget(target); } /// diff --git a/MLEM/Extensions/RandomExtensions.cs b/MLEM/Extensions/RandomExtensions.cs index 250e50c..b171c8c 100644 --- a/MLEM/Extensions/RandomExtensions.cs +++ b/MLEM/Extensions/RandomExtensions.cs @@ -16,8 +16,7 @@ namespace MLEM.Extensions { /// The entries' type /// A random entry public static T GetRandomEntry(this Random random, ICollection entries) { - // ElementAt internally optimizes for IList access so we don't have to here - return entries.ElementAt(random.Next(entries.Count)); + return RandomExtensions.GetRandomEntry(entries, random.NextSingle()); } /// @@ -31,28 +30,12 @@ namespace MLEM.Extensions { /// A random entry, based on the entries' weight /// If the weight function returns different weights for the same entry public static T GetRandomWeightedEntry(this Random random, ICollection entries, Func weightFunc) { - var totalWeight = entries.Sum(weightFunc); - var goalWeight = random.Next(totalWeight); - var currWeight = 0; - foreach (var entry in entries) { - currWeight += weightFunc(entry); - if (currWeight > goalWeight) - return entry; - } - throw new IndexOutOfRangeException(); + return RandomExtensions.GetRandomWeightedEntry(entries, weightFunc, random.NextSingle()); } /// public static T GetRandomWeightedEntry(this Random random, ICollection entries, Func weightFunc) { - var totalWeight = entries.Sum(weightFunc); - var goalWeight = random.NextDouble() * totalWeight; - var currWeight = 0F; - foreach (var entry in entries) { - currWeight += weightFunc(entry); - if (currWeight > goalWeight) - return entry; - } - throw new IndexOutOfRangeException(); + return RandomExtensions.GetRandomWeightedEntry(entries, weightFunc, random.NextSingle()); } /// @@ -87,5 +70,32 @@ namespace MLEM.Extensions { } #endif + internal static T GetRandomEntry(ICollection entries, float randomValue) { + // ElementAt internally optimizes for IList access so we don't have to here + return entries.ElementAt((int) (randomValue * entries.Count)); + } + + internal static T GetRandomWeightedEntry(ICollection entries, Func weightFunc, float randomValue) { + var goalWeight = randomValue * entries.Sum(weightFunc); + var currWeight = 0; + foreach (var entry in entries) { + currWeight += weightFunc(entry); + if (currWeight > goalWeight) + return entry; + } + throw new IndexOutOfRangeException(); + } + + internal static T GetRandomWeightedEntry(ICollection entries, Func weightFunc, float randomValue) { + var goalWeight = randomValue * entries.Sum(weightFunc); + var currWeight = 0F; + foreach (var entry in entries) { + currWeight += weightFunc(entry); + if (currWeight > goalWeight) + return entry; + } + throw new IndexOutOfRangeException(); + } + } } diff --git a/MLEM/Font/CodePointSource.cs b/MLEM/Font/CodePointSource.cs index fe4e3d2..9133e81 100644 --- a/MLEM/Font/CodePointSource.cs +++ b/MLEM/Font/CodePointSource.cs @@ -63,6 +63,18 @@ namespace MLEM.Font { return (curr, 1); } + /// + /// Returns an index in this code point source that is as close to as possible, but not between two members of a surrogate pair. If the is already not between surrogate pairs, it is returned unchanged. + /// + /// The index to ensure is not between surrogates. + /// Whether the returned index should be increased by 1 (instead of decreased by 1) when it is between surrogates. + /// An index close to , but not between surrogates. + public int EnsureSurrogateBoundary(int index, bool increase) { + if (index < this.Length && char.IsLowSurrogate(this[index])) + return increase || index <= 0 ? index + 1 : index - 1; + return index; + } + /// Returns an enumerator that iterates through the collection. /// A that can be used to iterate through the collection. /// 1 diff --git a/MLEM/Formatting/Codes/Code.cs b/MLEM/Formatting/Codes/Code.cs index 8b0f75e..393b149 100644 --- a/MLEM/Formatting/Codes/Code.cs +++ b/MLEM/Formatting/Codes/Code.cs @@ -23,9 +23,9 @@ namespace MLEM.Formatting.Codes { public readonly Match Match; /// /// The tokens that this formatting code is a part of. - /// Note that this array only has multiple entries if additional tokens have to be started while this code is still applied. + /// Note that this collection only has multiple entries if additional tokens have to be started while this code is still applied. /// - public IList Tokens { get; internal set; } + public readonly List Tokens = new List(); /// /// Creates a new formatting code based on a formatting code regex and its match. diff --git a/MLEM/Formatting/Codes/FontCode.cs b/MLEM/Formatting/Codes/FontCode.cs index 52e96c1..f1e437a 100644 --- a/MLEM/Formatting/Codes/FontCode.cs +++ b/MLEM/Formatting/Codes/FontCode.cs @@ -18,5 +18,11 @@ namespace MLEM.Formatting.Codes { return this.font?.Invoke(defaultPick); } + /// + public override bool EndsHere(Code other) { + // turning a string bold/italic should only end when that specific code is ended using SimpleEndCode + return false; + } + } } diff --git a/MLEM/Formatting/TextFormatter.cs b/MLEM/Formatting/TextFormatter.cs index f0789a7..f17124d 100644 --- a/MLEM/Formatting/TextFormatter.cs +++ b/MLEM/Formatting/TextFormatter.cs @@ -102,7 +102,7 @@ namespace MLEM.Formatting { this.Codes.Add(new Regex(""), (f, m, r) => new FontCode(m, r, fnt => fnt.Bold)); this.Codes.Add(new Regex(""), (f, m, r) => new FontCode(m, r, fnt => fnt.Italic)); this.Codes.Add(new Regex(@""), (f, m, r) => new ShadowCode(m, r, - m.Groups[1].Success ? ColorHelper.FromHexString(m.Groups[1].Value) : this.DefaultShadowColor, + ColorHelper.TryFromHexString(m.Groups[1].Value, out var color) ? color : this.DefaultShadowColor, float.TryParse(m.Groups[2].Value, NumberStyles.Number, CultureInfo.InvariantCulture, out var offset) ? new Vector2(offset) : this.DefaultShadowOffset)); this.Codes.Add(new Regex(""), (f, m, r) => new UnderlineCode(m, r, this.LineThickness, this.UnderlineOffset)); this.Codes.Add(new Regex(""), (f, m, r) => new UnderlineCode(m, r, this.LineThickness, this.StrikethroughOffset)); @@ -111,7 +111,7 @@ namespace MLEM.Formatting { this.Codes.Add(new Regex(@""), (f, m, r) => new SubSupCode(m, r, float.TryParse(m.Groups[1].Value, NumberStyles.Number, CultureInfo.InvariantCulture, out var off) ? -off : this.DefaultSupOffset)); this.Codes.Add(new Regex(@""), (f, m, r) => new OutlineCode(m, r, - m.Groups[1].Success ? ColorHelper.FromHexString(m.Groups[1].Value) : this.DefaultOutlineColor, + ColorHelper.TryFromHexString(m.Groups[1].Value, out var color) ? color : this.DefaultOutlineColor, float.TryParse(m.Groups[2].Value, NumberStyles.Number, CultureInfo.InvariantCulture, out var thickness) ? thickness : this.DefaultOutlineThickness, this.OutlineDiagonals)); } @@ -124,12 +124,13 @@ namespace MLEM.Formatting { this.Codes.Add(new Regex($""), (f, m, r) => new ColorCode(m, r, value)); } } - this.Codes.Add(new Regex(@""), (f, m, r) => new ColorCode(m, r, ColorHelper.FromHexString(m.Groups[1].Value))); + this.Codes.Add(new Regex(@""), (f, m, r) => new ColorCode(m, r, + ColorHelper.TryFromHexString(m.Groups[1].Value, out var color) ? color : Color.Red)); } // animation codes if (hasAnimations) { - this.Codes.Add(new Regex(@""), (f, m, r) => new WobblyCode(m, r, + this.Codes.Add(new Regex(""), (f, m, r) => new WobblyCode(m, r, float.TryParse(m.Groups[1].Value, NumberStyles.Number, CultureInfo.InvariantCulture, out var mod) ? mod : this.DefaultWobblyModifier, float.TryParse(m.Groups[2].Value, NumberStyles.Number, CultureInfo.InvariantCulture, out var heightMod) ? heightMod : this.DefaultWobblyHeight)); } @@ -155,11 +156,12 @@ namespace MLEM.Formatting { // resolve macros s = this.ResolveMacros(s); var tokens = new List(); - var codes = new List(); + var applied = new List(); + var allCodes = new List(); // add the formatting code right at the start of the string var firstCode = this.GetNextCode(s, 0, 0); if (firstCode != null) - codes.Add(firstCode); + applied.Add(firstCode); var index = 0; var rawIndex = 0; while (rawIndex < s.Length) { @@ -167,24 +169,25 @@ namespace MLEM.Formatting { // if we've reached the end of the string if (next == null) { var sub = s.Substring(rawIndex, s.Length - rawIndex); - tokens.Add(new Token(codes.ToArray(), index, rawIndex, TextFormatter.StripFormatting(font, sub, codes), sub)); + tokens.Add(new Token(applied.ToArray(), index, rawIndex, TextFormatter.StripFormatting(font, sub, applied), sub)); break; } + allCodes.Add(next); // create a new token for the content up to the next code var ret = s.Substring(rawIndex, next.Match.Index - rawIndex); - var strippedRet = TextFormatter.StripFormatting(font, ret, codes); - tokens.Add(new Token(codes.ToArray(), index, rawIndex, strippedRet, ret)); + var strippedRet = TextFormatter.StripFormatting(font, ret, applied); + tokens.Add(new Token(applied.ToArray(), index, rawIndex, strippedRet, ret)); // move to the start of the next code rawIndex = next.Match.Index; index += strippedRet.Length; // remove all codes that are incompatible with the next one and apply it - codes.RemoveAll(c => c.EndsHere(next) || next.EndsOther(c)); - codes.Add(next); + applied.RemoveAll(c => c.EndsHere(next) || next.EndsOther(c)); + applied.Add(next); } - return new TokenizedString(font, alignment, s, TextFormatter.StripFormatting(font, s, tokens.SelectMany(t => t.AppliedCodes)), tokens.ToArray()); + return new TokenizedString(font, alignment, s, TextFormatter.StripFormatting(font, s, allCodes), tokens.ToArray(), allCodes.ToArray()); } /// diff --git a/MLEM/Formatting/Token.cs b/MLEM/Formatting/Token.cs index 029c8ca..518f466 100644 --- a/MLEM/Formatting/Token.cs +++ b/MLEM/Formatting/Token.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using Microsoft.Xna.Framework; @@ -14,6 +15,7 @@ namespace MLEM.Formatting { /// /// The formatting codes that are applied on this token. + /// Codes are stored application order, with the first entry in the array being the code that was most recently applied. /// public readonly Code[] AppliedCodes; /// @@ -45,11 +47,14 @@ namespace MLEM.Formatting { internal float[] InnerOffsets; internal Token(Code[] appliedCodes, int index, int rawIndex, string substring, string rawSubstring) { + Array.Reverse(appliedCodes); this.AppliedCodes = appliedCodes; this.Index = index; this.RawIndex = rawIndex; this.Substring = substring; this.RawSubstring = rawSubstring; + foreach (var code in appliedCodes) + code.Tokens.Add(this); } /// diff --git a/MLEM/Formatting/TokenizedString.cs b/MLEM/Formatting/TokenizedString.cs index db49998..f27317b 100644 --- a/MLEM/Formatting/TokenizedString.cs +++ b/MLEM/Formatting/TokenizedString.cs @@ -1,11 +1,9 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Linq; using System.Text; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; -using MLEM.Extensions; using MLEM.Font; using MLEM.Formatting.Codes; using MLEM.Misc; @@ -42,17 +40,11 @@ namespace MLEM.Formatting { private float initialInnerOffset; private RectangleF area; - internal TokenizedString(GenericFont font, TextAlignment alignment, string rawString, string strg, Token[] tokens) { + internal TokenizedString(GenericFont font, TextAlignment alignment, string rawString, string strg, Token[] tokens, Code[] allCodes) { this.RawString = rawString; this.String = strg; this.Tokens = tokens; - - // since a code can be present in multiple tokens, we use Distinct here - this.AllCodes = tokens.SelectMany(t => t.AppliedCodes).Distinct().ToArray(); - // TODO this can probably be optimized by keeping track of a code's tokens while tokenizing - foreach (var code in this.AllCodes) - code.Tokens = new ReadOnlyCollection(this.Tokens.Where(t => t.AppliedCodes.Contains(code)).ToList()); - + this.AllCodes = allCodes; this.Realign(font, alignment); } diff --git a/MLEM/Graphics/AutoTiling.cs b/MLEM/Graphics/AutoTiling.cs index 6304304..fb5f3f0 100644 --- a/MLEM/Graphics/AutoTiling.cs +++ b/MLEM/Graphics/AutoTiling.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; +using MLEM.Misc; using MLEM.Textures; namespace MLEM.Graphics { @@ -86,92 +87,94 @@ namespace MLEM.Graphics { /// The layer depth to draw with. /// An optional depth offset from that the overlay should be drawn with public static void DrawExtendedAutoTile(SpriteBatch batch, Vector2 pos, TextureRegion backgroundTexture, TextureRegion overlayTexture, ConnectsTo connectsTo, Color backgroundColor, Color overlayColor, Vector2? origin = null, Vector2? scale = null, float layerDepth = 0, float overlayDepthOffset = 0) { - var orig = origin ?? Vector2.Zero; - var sc = scale ?? Vector2.One; - var od = layerDepth + overlayDepthOffset; - var (r1, r2, r3, r4) = AutoTiling.CalculateExtendedAutoTile(overlayTexture.Area, connectsTo); if (backgroundTexture != null) - batch.Draw(backgroundTexture, pos, backgroundColor, 0, orig, sc, SpriteEffects.None, layerDepth); - if (r1 != Rectangle.Empty) - batch.Draw(overlayTexture.Texture, pos, r1, overlayColor, 0, orig, sc, SpriteEffects.None, od); - if (r2 != Rectangle.Empty) - batch.Draw(overlayTexture.Texture, pos, r2, overlayColor, 0, orig, sc, SpriteEffects.None, od); - if (r3 != Rectangle.Empty) - batch.Draw(overlayTexture.Texture, pos, r3, overlayColor, 0, orig, sc, SpriteEffects.None, od); - if (r4 != Rectangle.Empty) - batch.Draw(overlayTexture.Texture, pos, r4, overlayColor, 0, orig, sc, SpriteEffects.None, od); + batch.Draw(backgroundTexture, pos, backgroundColor, 0, origin ?? Vector2.Zero, scale ?? Vector2.One, SpriteEffects.None, layerDepth); + var od = layerDepth + overlayDepthOffset; + AutoTiling.DrawExtendedAutoTileCorner(batch, pos, overlayTexture, connectsTo, overlayColor, Direction2.UpLeft, origin, scale, od); + AutoTiling.DrawExtendedAutoTileCorner(batch, pos, overlayTexture, connectsTo, overlayColor, Direction2.UpRight, origin, scale, od); + AutoTiling.DrawExtendedAutoTileCorner(batch, pos, overlayTexture, connectsTo, overlayColor, Direction2.DownLeft, origin, scale, od); + AutoTiling.DrawExtendedAutoTileCorner(batch, pos, overlayTexture, connectsTo, overlayColor, Direction2.DownRight, origin, scale, od); + } + + /// + /// This method allows for a single corner of a tiled texture to be drawn in an auto-tiling mode. + /// This allows, for example, a grass patch on a tilemap to have nice looking edges that transfer over into a path without any hard edges between tiles. + /// + /// For more information, and to draw all four corners at once, see + /// + /// The sprite batch to use for drawing. + /// The position to draw at. + /// The first overlay region, as described in the summary. + /// A function that determines whether two positions should connect. + /// The color to draw border and corner textures with. + /// The corner of the auto-tile to draw. Can be , , or . + /// The origin to draw from. + /// The scale to draw with. + /// The layer depth to draw with. + public static void DrawExtendedAutoTileCorner(SpriteBatch batch, Vector2 pos, TextureRegion overlayTexture, ConnectsTo connectsTo, Color overlayColor, Direction2 corner, Vector2? origin = null, Vector2? scale = null, float layerDepth = 0) { + var src = AutoTiling.CalculateExtendedAutoTile(overlayTexture.Area, connectsTo, corner); + if (src != Rectangle.Empty) + batch.Draw(overlayTexture.Texture, pos, src, overlayColor, 0, origin ?? Vector2.Zero, scale ?? Vector2.One, SpriteEffects.None, layerDepth); } /// public static void DrawExtendedAutoTile(SpriteBatch batch, Vector2 pos, TextureRegion backgroundTexture, Func overlayTextures, ConnectsTo connectsTo, Color backgroundColor, Color overlayColor, Vector2? origin = null, Vector2? scale = null, float layerDepth = 0, float overlayDepthOffset = 0) { - var orig = origin ?? Vector2.Zero; - var sc = scale ?? Vector2.One; - var od = layerDepth + overlayDepthOffset; - var (xUl, xUr, xDl, xDr) = AutoTiling.CalculateExtendedAutoTileOffsets(connectsTo); if (backgroundTexture != null) - batch.Draw(backgroundTexture, pos, backgroundColor, 0, orig, sc, SpriteEffects.None, layerDepth); - if (xUl >= 0) - batch.Draw(overlayTextures(xUl), pos, overlayColor, 0, orig, sc, SpriteEffects.None, od); - if (xUr >= 0) - batch.Draw(overlayTextures(xUr), pos, overlayColor, 0, orig, sc, SpriteEffects.None, od); - if (xDl >= 0) - batch.Draw(overlayTextures(xDl), pos, overlayColor, 0, orig, sc, SpriteEffects.None, od); - if (xDr >= 0) - batch.Draw(overlayTextures(xDr), pos, overlayColor, 0, orig, sc, SpriteEffects.None, od); + batch.Draw(backgroundTexture, pos, backgroundColor, 0, origin ?? Vector2.Zero, scale ?? Vector2.One, SpriteEffects.None, layerDepth); + var od = layerDepth + overlayDepthOffset; + AutoTiling.DrawExtendedAutoTileCorner(batch, pos, overlayTextures, connectsTo, overlayColor, Direction2.UpLeft, origin, scale, od); + AutoTiling.DrawExtendedAutoTileCorner(batch, pos, overlayTextures, connectsTo, overlayColor, Direction2.UpRight, origin, scale, od); + AutoTiling.DrawExtendedAutoTileCorner(batch, pos, overlayTextures, connectsTo, overlayColor, Direction2.DownLeft, origin, scale, od); + AutoTiling.DrawExtendedAutoTileCorner(batch, pos, overlayTextures, connectsTo, overlayColor, Direction2.DownRight, origin, scale, od); + } + + /// + public static void DrawExtendedAutoTileCorner(SpriteBatch batch, Vector2 pos, Func overlayTextures, ConnectsTo connectsTo, Color overlayColor, Direction2 corner, Vector2? origin = null, Vector2? scale = null, float layerDepth = 0) { + var src = AutoTiling.CalculateExtendedAutoTileOffset(connectsTo, corner); + if (src >= 0) + batch.Draw(overlayTextures(src), pos, overlayColor, 0, origin ?? Vector2.Zero, scale ?? Vector2.One, SpriteEffects.None, layerDepth); } /// public static void AddExtendedAutoTile(StaticSpriteBatch batch, Vector2 pos, TextureRegion backgroundTexture, TextureRegion overlayTexture, ConnectsTo connectsTo, Color backgroundColor, Color overlayColor, Vector2? origin = null, Vector2? scale = null, float layerDepth = 0, float overlayDepthOffset = 0, ICollection items = null) { - var orig = origin ?? Vector2.Zero; - var sc = scale ?? Vector2.One; - var od = layerDepth + overlayDepthOffset; - var (r1, r2, r3, r4) = AutoTiling.CalculateExtendedAutoTile(overlayTexture.Area, connectsTo); if (backgroundTexture != null) { - var background = batch.Add(backgroundTexture, pos, backgroundColor, 0, orig, sc, SpriteEffects.None, layerDepth); + var background = batch.Add(backgroundTexture, pos, backgroundColor, 0, origin ?? Vector2.Zero, scale ?? Vector2.One, SpriteEffects.None, layerDepth); items?.Add(background); } - if (r1 != Rectangle.Empty) { - var o1 = batch.Add(overlayTexture.Texture, pos, r1, overlayColor, 0, orig, sc, SpriteEffects.None, od); - items?.Add(o1); - } - if (r2 != Rectangle.Empty) { - var o2 = batch.Add(overlayTexture.Texture, pos, r2, overlayColor, 0, orig, sc, SpriteEffects.None, od); - items?.Add(o2); - } - if (r3 != Rectangle.Empty) { - var o3 = batch.Add(overlayTexture.Texture, pos, r3, overlayColor, 0, orig, sc, SpriteEffects.None, od); - items?.Add(o3); - } - if (r4 != Rectangle.Empty) { - var o4 = batch.Add(overlayTexture.Texture, pos, r4, overlayColor, 0, orig, sc, SpriteEffects.None, od); + var od = layerDepth + overlayDepthOffset; + AutoTiling.AddExtendedAutoTileCorner(batch, pos, overlayTexture, connectsTo, overlayColor, Direction2.UpLeft, origin, scale, od, items); + AutoTiling.AddExtendedAutoTileCorner(batch, pos, overlayTexture, connectsTo, overlayColor, Direction2.UpRight, origin, scale, od, items); + AutoTiling.AddExtendedAutoTileCorner(batch, pos, overlayTexture, connectsTo, overlayColor, Direction2.DownLeft, origin, scale, od, items); + AutoTiling.AddExtendedAutoTileCorner(batch, pos, overlayTexture, connectsTo, overlayColor, Direction2.DownRight, origin, scale, od, items); + } + + /// + public static void AddExtendedAutoTileCorner(StaticSpriteBatch batch, Vector2 pos, TextureRegion overlayTexture, ConnectsTo connectsTo, Color overlayColor, Direction2 corner, Vector2? origin = null, Vector2? scale = null, float layerDepth = 0, ICollection items = null) { + var src = AutoTiling.CalculateExtendedAutoTile(overlayTexture.Area, connectsTo, corner); + if (src != Rectangle.Empty) { + var o4 = batch.Add(overlayTexture.Texture, pos, src, overlayColor, 0, origin ?? Vector2.Zero, scale ?? Vector2.One, SpriteEffects.None, layerDepth); items?.Add(o4); } } /// public static void AddExtendedAutoTile(StaticSpriteBatch batch, Vector2 pos, TextureRegion backgroundTexture, Func overlayTextures, ConnectsTo connectsTo, Color backgroundColor, Color overlayColor, Vector2? origin = null, Vector2? scale = null, float layerDepth = 0, float overlayDepthOffset = 0, ICollection items = null) { - var orig = origin ?? Vector2.Zero; - var sc = scale ?? Vector2.One; - var od = layerDepth + overlayDepthOffset; - var (xUl, xUr, xDl, xDr) = AutoTiling.CalculateExtendedAutoTileOffsets(connectsTo); if (backgroundTexture != null) { - var background = batch.Add(backgroundTexture, pos, backgroundColor, 0, orig, sc, SpriteEffects.None, layerDepth); + var background = batch.Add(backgroundTexture, pos, backgroundColor, 0, origin ?? Vector2.Zero, scale ?? Vector2.One, SpriteEffects.None, layerDepth); items?.Add(background); } - if (xUl >= 0) { - var o1 = batch.Add(overlayTextures(xUl), pos, overlayColor, 0, orig, sc, SpriteEffects.None, od); - items?.Add(o1); - } - if (xUr >= 0) { - var o2 = batch.Add(overlayTextures(xUr), pos, overlayColor, 0, orig, sc, SpriteEffects.None, od); - items?.Add(o2); - } - if (xDl >= 0) { - var o3 = batch.Add(overlayTextures(xDl), pos, overlayColor, 0, orig, sc, SpriteEffects.None, od); - items?.Add(o3); - } - if (xDr >= 0) { - var o4 = batch.Add(overlayTextures(xDr), pos, overlayColor, 0, orig, sc, SpriteEffects.None, od); + var od = layerDepth + overlayDepthOffset; + AutoTiling.AddExtendedAutoTileCorner(batch, pos, overlayTextures, connectsTo, overlayColor, Direction2.UpLeft, origin, scale, od, items); + AutoTiling.AddExtendedAutoTileCorner(batch, pos, overlayTextures, connectsTo, overlayColor, Direction2.UpRight, origin, scale, od, items); + AutoTiling.AddExtendedAutoTileCorner(batch, pos, overlayTextures, connectsTo, overlayColor, Direction2.DownLeft, origin, scale, od, items); + AutoTiling.AddExtendedAutoTileCorner(batch, pos, overlayTextures, connectsTo, overlayColor, Direction2.DownRight, origin, scale, od, items); + } + + /// + public static void AddExtendedAutoTileCorner(StaticSpriteBatch batch, Vector2 pos, Func overlayTextures, ConnectsTo connectsTo, Color overlayColor, Direction2 corner, Vector2? origin = null, Vector2? scale = null, float layerDepth = 0, ICollection items = null) { + var src = AutoTiling.CalculateExtendedAutoTileOffset(connectsTo, corner); + if (src >= 0) { + var o4 = batch.Add(overlayTextures(src), pos, overlayColor, 0, origin ?? Vector2.Zero, scale ?? Vector2.One, SpriteEffects.None, layerDepth); items?.Add(o4); } } @@ -194,26 +197,36 @@ namespace MLEM.Graphics { new Vector2(pos.X + w2 * scale.X, pos.Y + h2 * scale.Y), new Rectangle(textureRegion.X + w2 + xDr * w, textureRegion.Y + h2, w2, h2)); } - private static (int, int, int, int) CalculateExtendedAutoTileOffsets(ConnectsTo connectsTo) { - var up = connectsTo(0, -1); - var down = connectsTo(0, 1); - var left = connectsTo(-1, 0); - var right = connectsTo(1, 0); - return ( - up && left ? connectsTo(-1, -1) ? -1 : 12 : left ? 0 : up ? 8 : 4, - up && right ? connectsTo(1, -1) ? -1 : 13 : right ? 1 : up ? 9 : 5, - down && left ? connectsTo(-1, 1) ? -1 : 14 : left ? 2 : down ? 10 : 6, - down && right ? connectsTo(1, 1) ? -1 : 15 : right ? 3 : down ? 11 : 7); + private static int CalculateExtendedAutoTileOffset(ConnectsTo connectsTo, Direction2 corner) { + switch (corner) { + case Direction2.UpLeft: { + var up = connectsTo(0, -1); + var left = connectsTo(-1, 0); + return up && left ? connectsTo(-1, -1) ? -1 : 12 : left ? 0 : up ? 8 : 4; + } + case Direction2.UpRight: { + var up = connectsTo(0, -1); + var right = connectsTo(1, 0); + return up && right ? connectsTo(1, -1) ? -1 : 13 : right ? 1 : up ? 9 : 5; + } + case Direction2.DownLeft: { + var down = connectsTo(0, 1); + var left = connectsTo(-1, 0); + return down && left ? connectsTo(-1, 1) ? -1 : 14 : left ? 2 : down ? 10 : 6; + } + case Direction2.DownRight: { + var down = connectsTo(0, 1); + var right = connectsTo(1, 0); + return down && right ? connectsTo(1, 1) ? -1 : 15 : right ? 3 : down ? 11 : 7; + } + default: + throw new ArgumentOutOfRangeException(nameof(corner), corner, null); + } } - private static (Rectangle, Rectangle, Rectangle, Rectangle) CalculateExtendedAutoTile(Rectangle textureRegion, ConnectsTo connectsTo) { - var (xUl, xUr, xDl, xDr) = AutoTiling.CalculateExtendedAutoTileOffsets(connectsTo); - var (w, h) = (textureRegion.Width, textureRegion.Height); - return ( - xUl < 0 ? Rectangle.Empty : new Rectangle(textureRegion.X + xUl * w, textureRegion.Y, w, h), - xUr < 0 ? Rectangle.Empty : new Rectangle(textureRegion.X + xUr * w, textureRegion.Y, w, h), - xDl < 0 ? Rectangle.Empty : new Rectangle(textureRegion.X + xDl * w, textureRegion.Y, w, h), - xDr < 0 ? Rectangle.Empty : new Rectangle(textureRegion.X + xDr * w, textureRegion.Y, w, h)); + private static Rectangle CalculateExtendedAutoTile(Rectangle textureRegion, ConnectsTo connectsTo, Direction2 corner) { + var off = AutoTiling.CalculateExtendedAutoTileOffset(connectsTo, corner); + return off < 0 ? Rectangle.Empty : new Rectangle(textureRegion.X + off * textureRegion.Width, textureRegion.Y, textureRegion.Width, textureRegion.Height); } /// diff --git a/MLEM/Input/InputHandler.cs b/MLEM/Input/InputHandler.cs index 5729513..c17ce25 100644 --- a/MLEM/Input/InputHandler.cs +++ b/MLEM/Input/InputHandler.cs @@ -121,11 +121,11 @@ namespace MLEM.Input { /// /// Contains the , but with the taken into account. /// - public IList LastViewportTouchState { get; private set; } + public IList LastViewportTouchState { get; private set; } = new List(); /// /// Contains the , but with the taken into account. /// - public IList ViewportTouchState { get; private set; } + public IList ViewportTouchState { get; private set; } = new List(); /// /// Contains the amount of gamepads that are currently connected. Note that this value will be set to 0 if is false. /// This field is automatically updated in . @@ -342,6 +342,7 @@ namespace MLEM.Input { } } else { this.TouchState = new TouchCollection(InputHandler.EmptyTouchLocations); + this.ViewportTouchState = this.TouchState; this.gestures.Clear(); } diff --git a/MLEM/Input/TextInput.cs b/MLEM/Input/TextInput.cs index f91fc87..3a45970 100644 --- a/MLEM/Input/TextInput.cs +++ b/MLEM/Input/TextInput.cs @@ -105,7 +105,8 @@ namespace MLEM.Input { set { var val = (int) MathHelper.Clamp(value, 0F, this.text.Length); if (this.caretPos != val) { - this.caretPos = val; + // ensure that we don't move to a location that is between high and low surrogates + this.caretPos = new CodePointSource(this.text).EnsureSurrogateBoundary(val, val > this.caretPos); this.caretBlinkTimer = 0; this.SetTextDataDirty(false); } @@ -203,6 +204,21 @@ namespace MLEM.Input { } } /// + /// The maximum amount of lines that can be visible in this text input, based on its , the used and its . + /// Note that this may return a number higher than 1 even if this is not a text input. + /// + public int MaxDisplayedLines => (this.Size.Y / (this.Font.LineHeight * this.TextScale)).Floor(); + /// + /// The index of the first line that is currently visible. + /// This value can be changed using . + /// + public int FirstVisibleLine { get; private set; } + /// + /// The total amount of lines of text that this text input currently has, including additional lines added by automatic wrapping. + /// If this is not a text input, this value is always 1. + /// + public int Lines { get; private set; } + /// /// A function that is invoked when a string of text should be copied to the clipboard. /// MLEM.Ui uses the TextCopy package for this, but other options are available. /// @@ -217,10 +233,9 @@ namespace MLEM.Input { private char? maskingCharacter; private double caretBlinkTimer; - private string displayedText; - private string[] splitText; + private string visibleText; + private string[] multilineSplitText; private int textOffset; - private int lineOffset; private int caretPos; private int caretLine; private int caretPosInLine; @@ -301,9 +316,9 @@ namespace MLEM.Input { this.CaretPos--; } else if (this.CaretPos < this.text.Length && input.TryConsumePressed(Keys.Right)) { this.CaretPos++; - } else if (this.Multiline && input.IsPressedAvailable(Keys.Up) && this.MoveCaretToLine(this.CaretLine - 1)) { + } else if (this.Multiline && input.IsPressedAvailable(Keys.Up) && (input.IsModifierKeyDown(ModifierKey.Control) ? this.ShowLine(this.FirstVisibleLine - 1) : this.MoveCaretToLine(this.CaretLine - 1))) { input.TryConsumePressed(Keys.Up); - } else if (this.Multiline && input.IsPressedAvailable(Keys.Down) && this.MoveCaretToLine(this.CaretLine + 1)) { + } else if (this.Multiline && input.IsPressedAvailable(Keys.Down) && (input.IsModifierKeyDown(ModifierKey.Control) ? this.ShowLine(this.FirstVisibleLine + 1) : this.MoveCaretToLine(this.CaretLine + 1))) { input.TryConsumePressed(Keys.Down); } else if (this.CaretPos != 0 && input.TryConsumePressed(Keys.Home)) { this.CaretPos = 0; @@ -339,12 +354,12 @@ namespace MLEM.Input { this.UpdateTextDataIfDirty(); var scale = this.TextScale * drawScale; - this.Font.DrawString(batch, this.displayedText, textPos, textColor, 0, Vector2.Zero, scale, SpriteEffects.None, 0); + this.Font.DrawString(batch, this.visibleText, textPos, textColor, 0, Vector2.Zero, scale, SpriteEffects.None, 0); if (caretWidth > 0 && this.caretBlinkTimer < 0.5F) { var caretDrawPos = textPos + new Vector2(this.caretDrawOffset * scale, 0); if (this.Multiline) - caretDrawPos.Y += this.Font.LineHeight * (this.CaretLine - this.lineOffset) * scale; + caretDrawPos.Y += this.Font.LineHeight * (this.CaretLine - this.FirstVisibleLine) * scale; batch.Draw(batch.GetBlankTexture(), new RectangleF(caretDrawPos, new Vector2(caretWidth * drawScale, this.Font.LineHeight * scale)), null, textColor); } } @@ -360,7 +375,7 @@ namespace MLEM.Input { if (!this.FilterText(ref strg, removeMismatching)) return; if (this.MaximumCharacters != null && strg.Length > this.MaximumCharacters) - strg = strg.Substring(0, this.MaximumCharacters.Value); + strg = strg.Substring(0, new CodePointSource(strg).EnsureSurrogateBoundary(this.MaximumCharacters.Value, false)); this.text.Clear(); this.text.Append(strg); this.CaretPos = this.text.Length; @@ -378,7 +393,7 @@ namespace MLEM.Input { if (!this.FilterText(ref strg, removeMismatching)) return false; if (this.MaximumCharacters != null && this.text.Length + strg.Length > this.MaximumCharacters) - strg = strg.Substring(0, this.MaximumCharacters.Value - this.text.Length); + strg = strg.Substring(0, new CodePointSource(strg).EnsureSurrogateBoundary(this.MaximumCharacters.Value - this.text.Length, false)); this.text.Insert(this.CaretPos, strg); this.CaretPos += strg.Length; this.SetTextDataDirty(); @@ -393,7 +408,8 @@ namespace MLEM.Input { public bool RemoveText(int index, int length) { if (index < 0 || index >= this.text.Length) return false; - this.text.Remove(index, length); + var source = new CodePointSource(this.text); + this.text.Remove(source.EnsureSurrogateBoundary(index, false), source.EnsureSurrogateBoundary(index + length, true) - index); // ensure that caret pos is still in bounds this.CaretPos = this.CaretPos; this.SetTextDataDirty(); @@ -417,7 +433,7 @@ namespace MLEM.Input { this.CaretPos = destStart + destAccum.Length; return true; } - destAccum += this.text[destStart + destAccum.Length]; + destAccum += CodePointSource.ToString(new CodePointSource(this.text).GetCodePoint(destStart + destAccum.Length).CodePoint); } // if we don't find a proper position, just move to the end of the destination line this.CaretPos = destEnd; @@ -426,6 +442,26 @@ namespace MLEM.Input { return false; } + /// + /// Moves visual focus into such bounds that the given line will be the first visible line of this text input. + /// + /// The first line that should be visible. + /// Whether the line can be the fist visible line, and wasn't already the first visible line. + public bool ShowLine(int line) { + if (this.FirstVisibleLine != line && line >= 0 && line < this.Lines - (this.MaxDisplayedLines - 1)) { + this.FirstVisibleLine = line; + + // move the caret into visible bounds if necessary + var clampedCaretLine = (int) MathHelper.Clamp(this.CaretLine, line, line + this.MaxDisplayedLines - 1F); + if (clampedCaretLine != this.CaretLine) + this.MoveCaretToLine(clampedCaretLine); + + this.SetTextDataDirty(false); + return true; + } + return false; + } + private bool FilterText(ref string text, bool removeMismatching) { var result = new StringBuilder(); foreach (var codePoint in new CodePointSource(text)) { @@ -458,49 +494,50 @@ namespace MLEM.Input { if (this.Multiline) { // soft wrap if we're multiline - this.splitText = this.Font.SplitStringSeparate(visualText, this.Size.X, this.TextScale).ToArray(); - this.displayedText = string.Join("\n", this.splitText); + this.multilineSplitText = this.Font.SplitStringSeparate(visualText, this.Size.X, this.TextScale).ToArray(); + this.visibleText = string.Join("\n", this.multilineSplitText); + this.Lines = this.visibleText.Count(c => c == '\n') + 1; this.UpdateCaretData(); - if (this.Font.MeasureString(this.displayedText).Y * this.TextScale > this.Size.Y) { - var maxLines = (this.Size.Y / (this.Font.LineHeight * this.TextScale)).Floor(); - if (this.lineOffset > this.CaretLine) { + if (this.Font.MeasureString(this.visibleText).Y * this.TextScale > this.Size.Y) { + if (this.FirstVisibleLine > this.CaretLine) { // if we're moving up - this.lineOffset = this.CaretLine; - } else if (this.CaretLine >= maxLines) { + this.FirstVisibleLine = this.CaretLine; + } else if (this.CaretLine >= this.MaxDisplayedLines) { // if we're moving down - var limit = this.CaretLine - (maxLines - 1); - if (limit > this.lineOffset) - this.lineOffset = limit; + var limit = this.CaretLine - (this.MaxDisplayedLines - 1); + if (limit > this.FirstVisibleLine) + this.FirstVisibleLine = limit; } // calculate resulting string var ret = new StringBuilder(); var lines = 0; var originalIndex = 0; - for (var i = 0; i < this.displayedText.Length; i++) { - if (lines >= this.lineOffset) { + for (var i = 0; i < this.visibleText.Length; i++) { + if (lines >= this.FirstVisibleLine) { if (ret.Length <= 0) this.textOffset = originalIndex; - ret.Append(this.displayedText[i]); + ret.Append(this.visibleText[i]); } - if (this.displayedText[i] == '\n') { + if (this.visibleText[i] == '\n') { lines++; if (visualText[originalIndex] == '\n') originalIndex++; } else { originalIndex++; } - if (lines - this.lineOffset >= maxLines) + if (lines - this.FirstVisibleLine >= this.MaxDisplayedLines) break; } - this.displayedText = ret.ToString(); + this.visibleText = ret.ToString(); } else { - this.lineOffset = 0; + this.FirstVisibleLine = 0; this.textOffset = 0; } } else { - this.splitText = null; - this.lineOffset = 0; + this.multilineSplitText = null; + this.FirstVisibleLine = 0; + this.Lines = 1; // not multiline, so scroll horizontally based on caret position if (this.Font.MeasureString(visualText).X * this.TextScale > this.Size.X) { if (this.textOffset > this.CaretPos) { @@ -514,9 +551,9 @@ namespace MLEM.Input { this.textOffset = bound; } var visible = visualText.ToString(this.textOffset, visualText.Length - this.textOffset); - this.displayedText = this.Font.TruncateString(visible, this.Size.X, this.TextScale); + this.visibleText = this.Font.TruncateString(visible, this.Size.X, this.TextScale); } else { - this.displayedText = visualText.ToString(); + this.visibleText = visualText.ToString(); this.textOffset = 0; } this.UpdateCaretData(); @@ -524,9 +561,9 @@ namespace MLEM.Input { } private void UpdateCaretData() { - if (this.splitText != null) { + if (this.multilineSplitText != null) { // the code below will never execute if our text is empty, so reset our caret position fully - if (this.splitText.Length <= 0) { + if (this.multilineSplitText.Length <= 0) { this.caretLine = 0; this.caretPosInLine = 0; this.caretDrawOffset = 0; @@ -535,9 +572,9 @@ namespace MLEM.Input { var line = 0; var index = 0; - for (var d = 0; d < this.splitText.Length; d++) { + for (var d = 0; d < this.multilineSplitText.Length; d++) { var startOfLine = 0; - var split = this.splitText[d]; + var split = this.multilineSplitText[d]; for (var i = 0; i <= split.Length; i++) { if (index == this.CaretPos) { this.caretLine = line; @@ -557,20 +594,20 @@ namespace MLEM.Input { // max width splits line++; } - } else if (this.displayedText != null) { + } else if (this.visibleText != null) { this.caretLine = 0; this.caretPosInLine = this.CaretPos; - this.caretDrawOffset = this.Font.MeasureString(this.displayedText.Substring(0, this.CaretPos - this.textOffset)).X; + this.caretDrawOffset = this.Font.MeasureString(this.visibleText.Substring(0, this.CaretPos - this.textOffset)).X; } } private (int, int) GetLineBounds(int boundLine) { - if (this.splitText != null) { + if (this.multilineSplitText != null) { var line = 0; var index = 0; var startOfLineIndex = 0; - for (var d = 0; d < this.splitText.Length; d++) { - var split = this.splitText[d]; + for (var d = 0; d < this.multilineSplitText.Length; d++) { + var split = this.multilineSplitText[d]; for (var i = 0; i < split.Length; i++) { index++; if (split[i] == '\n') { diff --git a/MLEM/MLEM.FNA.csproj b/MLEM/MLEM.FNA.csproj index 38c9b88..c4dff1f 100644 --- a/MLEM/MLEM.FNA.csproj +++ b/MLEM/MLEM.FNA.csproj @@ -1,9 +1,9 @@  - net452;netstandard2.0;net7.0 + net452;netstandard2.0;net8.0 true true - true + true MLEM $(DefineConstants);FNA @@ -21,7 +21,7 @@ - + all diff --git a/MLEM/MLEM.csproj b/MLEM/MLEM.csproj index b71a05a..9a55e0b 100644 --- a/MLEM/MLEM.csproj +++ b/MLEM/MLEM.csproj @@ -1,9 +1,9 @@  - net452;netstandard2.0;net7.0 + net452;netstandard2.0;net8.0 true true - true + true diff --git a/MLEM/Misc/Easings.cs b/MLEM/Misc/Easings.cs index a252c36..3c83b03 100644 --- a/MLEM/Misc/Easings.cs +++ b/MLEM/Misc/Easings.cs @@ -1,4 +1,5 @@ using System; +using Microsoft.Xna.Framework; namespace MLEM.Misc { /// @@ -8,6 +9,21 @@ namespace MLEM.Misc { /// public static class Easings { + /// + /// An easing function that constantly returns 0, regardless of the input percentage. + /// This is useful for chaining using . + /// + public static readonly Easing Zero = p => 0; + /// + /// An easing function that constantly returns 1, regardless of the input percentage. + /// This is useful for chaining using . + /// + public static readonly Easing One = p => 1; + /// + /// A linear easing function that returns the input percentage without modifying it. + /// + public static readonly Easing Linear = p => p; + /// https://easings.net/#easeInSine public static readonly Easing InSine = p => 1 - (float) Math.Cos(p * Math.PI / 2); /// https://easings.net/#easeOutSine @@ -170,6 +186,17 @@ namespace MLEM.Misc { }; } + /// + /// Causes output from the easing function to be clamped between the and values passed. + /// + /// The easing function to clamp. + /// The minimum output value to clamp to, defaults to 0. + /// The maximum output value to clamp to, defaults to 1. + /// A clamped easing function. + public static Easing Clamp(this Easing easing, float min = 0, float max = 1) { + return p => MathHelper.Clamp(easing(p), min, max); + } + /// /// A delegate method used by . /// diff --git a/MLEM/Misc/SingleRandom.cs b/MLEM/Misc/SingleRandom.cs index 73f7299..dc3d861 100644 --- a/MLEM/Misc/SingleRandom.cs +++ b/MLEM/Misc/SingleRandom.cs @@ -1,4 +1,8 @@ -namespace MLEM.Misc { +using System; +using System.Collections.Generic; +using MLEM.Extensions; + +namespace MLEM.Misc { /// /// The SingleRandom class allows generating single, one-off pseudorandom numbers based on a seed or a . /// The types of numbers that can be generated are and , both of which can be generated with specific minimum and maximum values if desired. @@ -138,5 +142,35 @@ return (maxValue - minValue) * SingleRandom.Single(source) + minValue; } + /// + /// Gets a random entry from the given collection with uniform chance. + /// + /// The entries to choose from + /// The to use. + /// The entries' type + /// A random entry + public static T GetRandomEntry(ICollection entries, SeedSource source) { + return RandomExtensions.GetRandomEntry(entries, SingleRandom.Single(source)); + } + + /// + /// Returns a random entry from the given collection based on the specified weight function. + /// A higher weight for an entry increases its likeliness of being picked. + /// + /// The entries to choose from + /// A function that applies weight to each entry + /// The to use. + /// The entries' type + /// A random entry, based on the entries' weight + /// If the weight function returns different weights for the same entry + public static T GetRandomWeightedEntry(ICollection entries, Func weightFunc, SeedSource source) { + return RandomExtensions.GetRandomWeightedEntry(entries, weightFunc, SingleRandom.Single(source)); + } + + /// + public static T GetRandomWeightedEntry(ICollection entries, Func weightFunc, SeedSource source) { + return RandomExtensions.GetRandomWeightedEntry(entries, weightFunc, SingleRandom.Single(source)); + } + } } diff --git a/MLEM/Textures/NinePatch.cs b/MLEM/Textures/NinePatch.cs index 1a3481c..827af62 100644 --- a/MLEM/Textures/NinePatch.cs +++ b/MLEM/Textures/NinePatch.cs @@ -158,11 +158,13 @@ namespace MLEM.Textures { case NinePatchMode.Tile: var width = src.Width * patchScale; var height = src.Height * patchScale; - for (var x = 0F; x < rect.Width; x += width) { - for (var y = 0F; y < rect.Height; y += height) { - var size = new Vector2(Math.Min(rect.Width - x, width), Math.Min(rect.Height - y, height)); - var srcSize = (size / patchScale).CeilCopy().ToPoint(); - batch.Draw(texture.Region.Texture, new RectangleF(rect.Location + new Vector2(x, y), size), new Rectangle(src.X, src.Y, srcSize.X, srcSize.Y), color, rotation, origin, effects, layerDepth); + if (width > 0 && height > 0) { + for (var x = 0F; x < rect.Width; x += width) { + for (var y = 0F; y < rect.Height; y += height) { + var size = new Vector2(Math.Min(rect.Width - x, width), Math.Min(rect.Height - y, height)); + var srcSize = (size / patchScale).CeilCopy().ToPoint(); + batch.Draw(texture.Region.Texture, new RectangleF(rect.Location + new Vector2(x, y), size), new Rectangle(src.X, src.Y, srcSize.X, srcSize.Y), color, rotation, origin, effects, layerDepth); + } } } break; diff --git a/NuGet.config b/NuGet.config deleted file mode 100644 index c6e98bd..0000000 --- a/NuGet.config +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/README.md b/README.md index 103927a..06abb46 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ MLEM is platform-agnostic and multi-targets .NET Standard 2.0, .NET 6.0 and .NET - See tutorials and API documentation on [the website](https://mlem.ellpeck.de/) - Check out [the demos](https://github.com/Ellpeck/MLEM/tree/main/Demos) on [Desktop](https://github.com/Ellpeck/MLEM/tree/main/Demos.DesktopGL) or [Android](https://github.com/Ellpeck/MLEM/tree/main/Demos.Android) - See [the changelog](https://github.com/Ellpeck/MLEM/blob/main/CHANGELOG.md) for information on updates +- Join [the Discord server](https://link.ellpeck.de/discordweb) to ask questions # Packages - **MLEM** is the base package, which provides various small addons and abstractions for MonoGame and FNA, including a text formatting system and simple input handling diff --git a/Sandbox/Content/Content.mgcb b/Sandbox/Content/Content.mgcb index 8c80f5f..62f647f 100644 --- a/Sandbox/Content/Content.mgcb +++ b/Sandbox/Content/Content.mgcb @@ -10,8 +10,6 @@ #-------------------------------- References --------------------------------# -/reference:..\..\packages\monogame.extended.content.pipeline\3.8.0\tools\MonoGame.Extended.Content.Pipeline.dll - #---------------------------------- Content ---------------------------------# #begin Fonts/Cadman_Roman.otf @@ -20,11 +18,6 @@ #begin Fonts/Symbola-Emoji.ttf /copy:Fonts/Symbola-Emoji.ttf -#begin Fonts/Regular.fnt -/importer:BitmapFontImporter -/processor:BitmapFontProcessor -/build:Fonts/Regular.fnt - #begin Fonts/RegularTexture.png /importer:TextureImporter /processor:TextureProcessor @@ -64,5 +57,3 @@ #begin Textures/Test.png /copy:Textures/Test.png - - diff --git a/Sandbox/Content/Fonts/Regular.fnt b/Sandbox/Content/Fonts/Regular.fnt deleted file mode 100644 index dd46e16..0000000 --- a/Sandbox/Content/Fonts/Regular.fnt +++ /dev/null @@ -1,330 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sandbox/Content/Tiled/Map.tmx b/Sandbox/Content/Tiled/Map.tmx deleted file mode 100644 index 55745ce..0000000 --- a/Sandbox/Content/Tiled/Map.tmx +++ /dev/null @@ -1,274 +0,0 @@ - - - - - - 450,450,450,450,450,451,3,36,38,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,132,133,71,102,3,3,3,3,3,3,3,3,3,3,3,3,3, - 450,450,450,450,450,451,3,36,38,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,132,133,133,134,3,3,3,3,3,3,3,3,3,3,3,3,3, - 450,450,450,450,420,483,3,36,38,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,164,40,133,134,3,3,3,3,3,3,3,3,3,3,3,3,3, - 450,450,450,450,451,3,3,36,38,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,132,133,134,3,3,3,3,3,3,3,3,3,3,3,3,3, - 450,450,450,450,451,3,3,36,38,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,132,133,134,3,3,3,3,3,3,3,3,3,3,3,3,3, - 450,450,450,450,451,3,3,36,38,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,132,133,134,3,3,3,3,3,3,3,3,3,3,3,3,3, - 450,450,450,420,483,3,3,36,38,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,100,72,133,134,3,3,3,3,3,3,3,3,3,3,3,3,3, - 450,450,450,451,3,3,3,36,38,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,100,72,133,133,134,3,3,3,3,3,3,3,3,3,3,3,3,3, - 450,450,450,451,3,3,3,36,38,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,100,72,133,133,39,166,3,3,3,3,3,3,3,3,3,3,3,3,3, - 450,450,420,483,3,3,3,36,38,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,100,72,133,133,39,166,3,3,3,3,3,3,3,3,3,3,3,3,3,3, - 450,450,451,3,3,3,3,36,38,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,132,133,133,39,166,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, - 450,450,452,419,3,3,3,36,38,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,132,133,133,134,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, - 450,450,450,451,3,3,3,36,38,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,132,133,133,134,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, - 450,450,450,452,419,3,3,36,38,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,132,133,39,166,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, - 450,450,450,450,451,3,3,36,38,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,132,133,134,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, - 450,450,450,450,451,3,3,36,38,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,132,133,134,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, - 450,450,450,450,451,3,3,36,66,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5, - 450,450,450,420,483,3,3,68,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,35,37,37,37,37,37,37,37,37,37,37,37,37,37,37,37, - 450,450,450,451,3,3,3,3,3,3,3,132,133,134,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,36,37,34,69,69,69,69,69,69,69,69,69,69,69,69,69, - 450,450,450,452,419,3,3,3,3,3,100,72,133,134,3,3,3,3,3,3,3,3,3,3,3,3,3,417,418,419,3,3,3,3,36,37,38,3,3,3,3,3,3,3,3,3,3,3,417,418, - 450,450,450,450,451,3,3,3,3,3,132,133,133,134,3,3,3,3,3,3,3,3,3,3,3,3,417,453,450,451,3,3,3,3,36,37,38,3,3,3,3,3,3,3,3,3,3,417,453,2684355010, - 450,450,450,450,451,3,3,3,3,3,132,133,133,134,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,36,37,38,3,3,3,3,3,3,3,3,3,417,453,2684355010,2684355010, - 450,450,450,450,451,3,3,3,3,3,164,40,133,71,102,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,36,37,38,3,3,3,3,3,3,3,3,3,449,2684355010,2684355010,2684355010, - 450,450,450,420,483,3,3,3,3,3,3,132,133,133,134,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,36,37,38,3,3,3,3,3,3,3,3,417,453,2684355010,2684355010,2684355010, - 450,450,450,451,3,3,3,3,3,3,3,164,40,133,71,102,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,36,37,38,3,3,3,3,3,3,3,417,453,2684355010,2684355010,2684355010,2684355010, - 450,450,420,483,3,3,3,3,3,3,3,3,132,133,133,71,102,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,36,37,38,3,3,3,3,3,3,3,449,2684355010,2684355010,2684355010,2684355010,2684355010, - 450,420,483,3,3,3,3,3,3,3,3,3,164,40,133,133,134,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,36,37,38,3,417,418,418,419,3,3,449,2684355010,2684355010,2684355010,2684355010,2684355010, - 450,451,3,3,3,3,3,3,3,3,3,3,3,164,40,133,134,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,36,37,38,3,449,2684355010,2684355010,452,419,3,449,2684355010,2684355010,2684355010,2684355010,2684355010, - 450,451,3,3,3,3,3,3,3,3,3,3,3,3,132,133,134,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,36,37,38,417,453,2684355010,2684355010,2684355010,451,3,449,2684355010,2684355010,2684355010,2684355010,2684355010, - 450,451,3,3,3,3,3,3,3,3,3,3,3,3,132,133,134,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,36,37,38,449,2684355010,2684355010,2684355010,2684355010,451,3,449,2684355010,2684355010,2684355010,2684355010,2684355010, - 450,452,419,3,3,3,3,3,3,3,3,3,3,3,132,133,134,3,3,3,3,3,3,3,36,37,38,3,3,3,3,3,3,3,36,37,38,449,2684355010,2684355010,2684355010,2684355010,451,3,481,482,421,2684355010,2684355010,2684355010, - 450,450,451,3,3,3,3,3,3,3,3,3,3,3,132,133,71,102,3,100,101,101,101,101,36,37,66,5,5,5,5,5,5,5,67,37,38,449,2684355010,2684355010,2684355010,2684355010,451,3,3,3,481,482,482,482, - 450,450,451,3,3,3,3,3,3,3,3,3,3,100,72,133,133,71,101,72,133,133,133,133,36,37,37,37,37,37,37,37,37,37,37,37,38,481,421,2684355010,2684355010,420,483,3,3,3,3,16,16,16, - 450,450,451,3,3,3,3,3,3,3,3,3,100,72,133,133,133,133,133,133,133,39,165,165,68,69,69,69,69,69,69,69,69,69,69,69,70,3,481,482,482,483,3,3,16,16,16,16,16,16, - 450,450,451,3,3,3,132,133,71,101,101,101,72,133,133,133,39,165,165,165,165,166,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,16,16,16,16,16,16,16, - 450,450,451,3,3,3,132,133,133,133,133,133,133,133,39,165,166,3,3,3,3,3,3,3,417,418,418,418,418,418,418,418,418,418,418,419,3,3,3,3,3,16,16,16,16,16,16,16,16,16, - 450,450,452,419,3,3,164,165,165,165,165,165,165,165,166,3,3,3,3,3,3,3,3,417,453,2684355010,2684355010,2684355010,2684355010,2684355010,2684355010,2684355010,2684355010,2684355010,2684355010,451,3,3,3,3,16,16,16,16,16,16,16,16,16,16, - 450,450,450,451,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,481,421,2684355010,2684355010,2684355010,2684355010,2684355010,2684355010,2684355010,2684355010,2684355010,420,483,3,16,16,16,16,16,16,16,16,16,16,16,16,16, - 450,450,450,451,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,481,482,482,482,482,482,482,482,482,482,483,3,16,16,16,16,16,16,16,16,16,16,16,16,16,16, - 450,450,450,451,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16, - 450,450,450,451,3,3,3,3,3,3,3,3,3,3,3,3,3,3,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,3, - 450,450,450,451,3,3,3,3,3,3,3,3,3,3,3,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,3,3,3, - 2684355010,450,420,483,3,3,3,3,3,3,3,3,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,3,3,3,3,3, - 2684355010,420,483,3,3,3,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,3,3,3,3,3,3, - 482,483,3,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,3,3,3,3,3,3,3,3, - 3,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,3,3,3,3,3,3,3,3,3,3,3, - 16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,3,3,3,3,3,3,3,3,3,3,3,3, - 16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, - 16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, - 16,16,16,16,16,16,16,16,16,16,16,16,16,16,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3 - - - - - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,17,17,17, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,17,17,17,17,17,17, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,17,17,17,17,17,17,18, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,17,17,17,17,17,18,19,19,54, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,17,17,17,18,19,19,54,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,17,17,17,17,17,18,54,0,0,0,21,83,83, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,17,17,17,17,18,19,54,0,0,21,83,84,17,17, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,17,17,17,17,17,17,18,54,0,0,21,83,84,17,17,17,17, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,18,19,54,0,0,21,84,17,17,17,17,17,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,18,19,54,0,0,0,21,84,17,17,17,17,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,17,17,17,17,17,17,17,18,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,54,0,21,83,83,83,84,17,17,17,0,0,0,0,0, - 0,0,0,0,0,0,17,17,17,17,17,17,17,17,17,17,17,18,19,54,0,0,0,0,0,0,0,0,0,0,0,0,0,21,83,83,83,84,17,17,17,17,17,17,0,0,0,0,0,0, - 0,0,0,17,17,17,17,17,17,17,17,17,17,17,18,19,19,54,0,0,0,0,0,0,0,0,21,83,83,83,83,83,83,84,17,17,17,17,17,17,17,17,0,0,0,0,0,0,0,0, - 0,17,17,17,17,17,17,17,17,17,17,18,19,19,54,0,0,0,21,83,83,83,83,83,83,83,84,17,17,17,17,17,17,17,17,17,17,17,17,0,0,0,0,0,0,0,0,0,0,0, - 17,17,17,17,17,17,18,19,19,19,19,54,0,0,21,83,83,83,84,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,0,0,0,0,0,0,0,0,0,0,0,0, - 17,17,18,19,19,19,54,0,0,0,21,83,83,83,84,17,17,17,17,17,17,17,17,17,17,17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 19,19,54,0,0,0,21,83,83,83,84,17,17,17,17,17,17,17,17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,21,84,17,17,17,17,17,17,17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 - - - - - 0,0,0,0,0,397,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,397,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,365,430,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,397,0,237,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,397,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,397,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,397,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,368,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,397,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,397,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,397,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,397,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,397,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,397,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,368,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,397,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,237,0, - 0,0,0,0,397,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,397,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,397,0,237,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,303,0,397,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,366,366, - 0,0,0,0,397,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,301,302,0,0,0,0,0,0,0,0,0,0,0,0,0,0,397,0,0, - 0,0,0,0,397,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,397,0,0, - 0,0,0,0,397,0,0,0,0,0,0,0,0,0,0,0,0,303,0,0,0,0,199,198,198,198,198,198,199,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,368,0,0, - 0,365,366,366,430,0,0,0,0,0,0,0,0,0,0,0,0,0,197,198,198,198,199,198,198,198,198,198,199,198,198,198,200,0,0,0,0,0,0,0,303,0,0,0,0,0,0,397,0,0, - 0,397,0,301,302,0,0,0,0,0,0,0,0,0,0,0,0,0,197,198,198,198,199,198,198,198,198,198,199,198,198,198,200,0,0,0,0,0,237,0,0,0,0,0,0,0,0,397,0,0, - 0,397,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,265,266,266,266,267,266,266,266,266,266,267,266,266,266,268,0,0,0,0,0,111,0,0,0,0,0,0,0,0,397,0,0, - 0,397,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,197,198,198,198,199,198,198,198,198,198,199,198,198,198,200,0,0,0,0,0,77,0,0,0,0,0,0,365,366,430,0,0, - 0,397,0,0,0,0,0,0,0,0,0,301,302,0,0,0,0,0,197,198,198,198,199,198,198,198,198,198,199,198,198,198,200,0,0,0,0,0,77,0,0,0,0,0,0,397,0,0,303,0, - 303,397,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,265,266,266,266,267,266,266,290,266,266,267,266,266,266,268,0,0,0,0,0,77,0,0,0,0,0,0,397,0,0,0,0, - 0,368,0,193,194,195,194,194,194,195,194,196,0,0,0,0,0,0,197,198,198,198,199,198,198,322,198,198,199,198,198,198,200,0,0,0,0,0,77,0,0,0,0,0,0,397,0,0,0,0, - 0,397,0,193,194,195,194,194,194,195,194,196,0,0,0,0,0,0,0,7,7,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,77,0,301,302,0,0,0,397,0,0,0,0, - 0,397,0,193,194,195,194,194,194,195,194,196,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,77,0,0,0,0,0,0,397,0,0,0,0, - 0,397,0,193,194,195,194,290,194,195,194,196,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,77,0,0,0,0,0,0,399,0,97,98,98, - 0,397,0,193,194,195,194,322,194,195,194,196,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,77,0,0,0,0,0,97,98,98,74,0,0, - 0,397,301,225,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,77,0,0,0,0,97,74,0,0,0,0,0, - 0,397,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,237,109,46,46,46,46,46,46,46,46,46,46,46,46,46,110,0,0,97,98,74,0,0,0,0,0,0, - 0,397,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,303,0,0,0,0,0,0,301,302,0,0,0,0,0,97,74,0,0,0,0,0,0,0,0, - 0,429,366,367,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,97,98,98,74,0,0,0,0,0,0,0,0,0, - 0,0,0,397,0,0,0,0,0,0,0,0,301,302,0,0,0,0,0,0,0,301,302,0,0,0,0,0,0,0,0,0,0,0,0,0,97,74,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,397,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,97,98,98,74,0,0,0,0,0,0,0,0,0,0,0,41,162, - 0,301,302,397,0,0,0,0,0,0,0,0,0,0,0,0,0,0,97,98,98,98,98,98,98,98,98,98,98,98,98,98,98,74,0,0,0,0,0,0,0,0,0,0,0,0,41,162,163,0, - 0,0,0,397,0,0,0,0,0,0,0,0,0,0,0,97,98,98,74,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,41,162,163,0,0,0, - 0,0,0,397,0,0,0,0,0,0,0,0,97,98,98,74,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,41,163,0,0,0,0,0, - 0,0,0,399,0,303,97,98,98,98,98,98,74,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,41,162,163,0,0,0,301,302,0, - 0,0,0,97,98,98,74,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,41,162,162,163,0,0,0,0,0,0,0,0, - 0,97,98,74,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,41,163,0,0,0,0,0,0,0,0,0,0,0, - 98,74,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,41,162,162,162,162,162,162,162,162,162,162,162,163,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,41,162,162,162,162,162,162,163,0,0,301,302,0,0,0,0,0,0,0,0,303,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,41,162,162,162,162,163,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,303,0,0,0,0, - 0,0,0,0,0,0,0,0,41,162,162,162,162,163,0,0,0,0,0,0,0,303,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 - - - - - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,237,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,7,7,7,7,7,7,7,7,7,365,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,298,299,300,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,330,331,332,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,362,363,364,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,385,386,387,0,354,0,0,0,354,0,385,386,387,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,103,104,105,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,385,386,387,0,0,135,0,137,0,0,385,386,387,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,135,0,137,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,385,386,387,0,0,0,0,0,0,0,0,0,229,301,302,230,231,230,230,230,230,230,231,303,230,230,232,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,257,258,259,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,354,0,289,0,291,0,354,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,289,0,291,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,302,226,227,226,226,226,227,226,228,0,0,0,0,0,0,0,0,0,0,0,0,7,7,7,7,7,7,7,7,7,7,7,7,7,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 - - - - - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,333,335,173,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,205,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,336,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,336,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,173,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,205,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,173,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,173,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,205,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,205,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,271,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,333,334,334, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,261,263,264,0,0,0,0,269,270,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,261,262,262,262,262,293,327,296,262,262,262,262,264,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,271,261,293,326,326,326,326,325,327,328,326,326,326,326,296,264,0,0,0,0,0,0,0,0,0,0,0,0,0,0,336,0,0, - 0,333,334,334,335,0,0,0,0,0,0,0,0,0,0,0,0,292,293,325,326,326,326,326,325,327,328,326,326,326,326,328,296,297,0,0,0,0,173,0,271,0,0,0,0,0,0,0,0,0, - 0,0,0,269,270,0,0,0,0,0,0,0,0,0,0,0,0,356,325,325,326,326,326,326,357,359,360,326,326,326,326,328,328,361,0,0,0,0,205,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,356,325,357,358,358,358,358,389,0,392,358,358,358,358,360,328,361,0,0,0,0,79,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,356,357,389,0,0,0,0,0,0,0,0,0,0,0,392,360,361,0,0,0,0,0,0,0,0,0,0,0,333,334,335,0,0, - 0,0,0,0,0,261,262,262,262,264,0,269,270,0,0,0,0,388,389,0,0,0,0,0,0,0,0,0,0,0,0,0,392,393,0,0,0,0,0,0,0,0,0,0,0,0,0,0,271,0, - 271,0,0,261,262,293,326,326,326,296,262,264,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,336,292,293,326,325,326,326,326,328,326,296,297,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,356,325,326,357,358,358,358,360,326,328,361,0,0,0,0,0,0,269,270,0,0,0,0,0,0,0,0,271,0,0,0,0,0,0,0,0,0,0,269,270,0,0,0,0,0,0,0,0, - 0,0,356,357,358,389,0,0,0,392,358,360,361,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,388,389,0,0,0,0,0,0,0,392,393,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,43,43,43,43,43,43,43,0,0,0,0,0,0,0,0,336,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,43,8,2147483656,9,1073741833,3221225545,43,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,269,270,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,173,0,0,0,0,0,0,43,43,43,43,43,43,43,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,205,13,14,14,14,14,14,14,14,14,14,14,14,14,14,15,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,271,0,0,0,0,0,0,269,270,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,333,334,335,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,269,270,0,0,0,0,0,0,0,269,270,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,269,270,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,336,0,271,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,269,270,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,269,270,0,0,0,0,0,0,0,0,271,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,271,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,271,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 - - - diff --git a/Sandbox/Content/Tiled/Tiles.png b/Sandbox/Content/Tiled/Tiles.png deleted file mode 100644 index bc1175d..0000000 Binary files a/Sandbox/Content/Tiled/Tiles.png and /dev/null differ diff --git a/Sandbox/Content/Tiled/Tileset.tsx b/Sandbox/Content/Tiled/Tileset.tsx deleted file mode 100644 index 8e6435e..0000000 --- a/Sandbox/Content/Tiled/Tileset.tsx +++ /dev/null @@ -1,123 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sandbox/Sandbox.csproj b/Sandbox/Sandbox.csproj index ec2d3f8..a1b5759 100644 --- a/Sandbox/Sandbox.csproj +++ b/Sandbox/Sandbox.csproj @@ -2,7 +2,7 @@ Exe - net7.0 + net8.0 false @@ -19,16 +19,11 @@ - + - - - - - diff --git a/Tests/Tests.FNA.csproj b/Tests/Tests.FNA.csproj index 73db59b..ed57fb9 100644 --- a/Tests/Tests.FNA.csproj +++ b/Tests/Tests.FNA.csproj @@ -1,6 +1,6 @@ - net7.0 + net8.0 nunit TestResults.FNA Tests.FNA.runsettings @@ -18,24 +18,21 @@ - - + + - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - + + + + + PreserveNewest - + PreserveNewest %(Filename)%(Extension) diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index c224dd3..013fba0 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -1,6 +1,6 @@ - net7.0 + net8.0 nunit TestResults Tests.runsettings @@ -20,14 +20,11 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - + + + + + diff --git a/ThirdParty/FNA b/ThirdParty/FNA new file mode 160000 index 0000000..354e216 --- /dev/null +++ b/ThirdParty/FNA @@ -0,0 +1 @@ +Subproject commit 354e2161b759fa052b25e94209d6ea463aaf098f diff --git a/ThirdParty/FontStashSharp b/ThirdParty/FontStashSharp new file mode 160000 index 0000000..2d40e9f --- /dev/null +++ b/ThirdParty/FontStashSharp @@ -0,0 +1 @@ +Subproject commit 2d40e9f0f681595dbd4341a3e5a64ed6e31f9556 diff --git a/ThirdParty/Native/FAudio.dll b/ThirdParty/Native/FAudio.dll new file mode 100644 index 0000000..26fe60f Binary files /dev/null and b/ThirdParty/Native/FAudio.dll differ diff --git a/ThirdParty/Native/FNA3D.dll b/ThirdParty/Native/FNA3D.dll new file mode 100644 index 0000000..5ba4648 Binary files /dev/null and b/ThirdParty/Native/FNA3D.dll differ diff --git a/FnaNative/SDL2.dll b/ThirdParty/Native/SDL2.dll similarity index 58% rename from FnaNative/SDL2.dll rename to ThirdParty/Native/SDL2.dll index 0f91bd6..4e9bd60 100644 Binary files a/FnaNative/SDL2.dll and b/ThirdParty/Native/SDL2.dll differ diff --git a/ThirdParty/Native/libFAudio.0.dylib b/ThirdParty/Native/libFAudio.0.dylib new file mode 100644 index 0000000..f03f782 Binary files /dev/null and b/ThirdParty/Native/libFAudio.0.dylib differ diff --git a/ThirdParty/Native/libFAudio.so.0 b/ThirdParty/Native/libFAudio.so.0 new file mode 100644 index 0000000..fbc806f Binary files /dev/null and b/ThirdParty/Native/libFAudio.so.0 differ diff --git a/ThirdParty/Native/libFNA3D.0.dylib b/ThirdParty/Native/libFNA3D.0.dylib new file mode 100644 index 0000000..474c271 Binary files /dev/null and b/ThirdParty/Native/libFNA3D.0.dylib differ diff --git a/ThirdParty/Native/libFNA3D.so.0 b/ThirdParty/Native/libFNA3D.so.0 new file mode 100644 index 0000000..1d01979 Binary files /dev/null and b/ThirdParty/Native/libFNA3D.so.0 differ diff --git a/FnaNative/libMoltenVK.dylib b/ThirdParty/Native/libMoltenVK.dylib similarity index 100% rename from FnaNative/libMoltenVK.dylib rename to ThirdParty/Native/libMoltenVK.dylib diff --git a/ThirdParty/Native/libSDL2-2.0.0.dylib b/ThirdParty/Native/libSDL2-2.0.0.dylib new file mode 100644 index 0000000..fced8da Binary files /dev/null and b/ThirdParty/Native/libSDL2-2.0.0.dylib differ diff --git a/ThirdParty/Native/libSDL2-2.0.so.0 b/ThirdParty/Native/libSDL2-2.0.so.0 new file mode 100644 index 0000000..f571b83 Binary files /dev/null and b/ThirdParty/Native/libSDL2-2.0.so.0 differ diff --git a/FnaNative/libtheorafile.dll b/ThirdParty/Native/libtheorafile.dll similarity index 100% rename from FnaNative/libtheorafile.dll rename to ThirdParty/Native/libtheorafile.dll diff --git a/FnaNative/libtheorafile.dylib b/ThirdParty/Native/libtheorafile.dylib similarity index 100% rename from FnaNative/libtheorafile.dylib rename to ThirdParty/Native/libtheorafile.dylib diff --git a/FnaNative/libtheorafile.so b/ThirdParty/Native/libtheorafile.so similarity index 100% rename from FnaNative/libtheorafile.so rename to ThirdParty/Native/libtheorafile.so diff --git a/FnaNative/libvulkan.1.dylib b/ThirdParty/Native/libvulkan.1.dylib similarity index 100% rename from FnaNative/libvulkan.1.dylib rename to ThirdParty/Native/libvulkan.1.dylib diff --git a/build.cake b/build.cake index 9a329eb..1966ffb 100644 --- a/build.cake +++ b/build.cake @@ -2,7 +2,7 @@ #tool dotnet:?package=docfx&version=2.70.3 // this is the upcoming version, for prereleases -var version = Argument("version", "6.2.0"); +var version = Argument("version", "6.3.0"); var target = Argument("target", "Default"); var branch = Argument("branch", "main"); var config = Argument("configuration", "Release"); @@ -14,8 +14,8 @@ Task("Prepare").Does(() => { DotNetRestore("MLEM.FNA.sln"); if (branch != "release") { - var buildNum = EnvironmentVariable("CI_PIPELINE_NUMBER"); - if (buildNum != null) + var buildNum = EnvironmentVariable("GITHUB_RUN_NUMBER"); + if (!string.IsNullOrEmpty(buildNum)) version += "-ci." + buildNum; } @@ -25,7 +25,9 @@ Task("Prepare").Does(() => { Task("Build").IsDependentOn("Prepare").Does(() =>{ var settings = new DotNetBuildSettings { Configuration = config, - ArgumentCustomization = args => args.Append($"/p:Version={version}") + ArgumentCustomization = args => args.Append($"/p:Version={version}"), + // .net 8 has an issue that causes simultaneous tool restores during build to fail + MSBuildSettings = new DotNetMSBuildSettings { MaxCpuCount = 1 } }; DotNetBuild("MLEM.sln", settings); DotNetBuild("MLEM.FNA.sln", settings); @@ -34,7 +36,8 @@ Task("Build").IsDependentOn("Prepare").Does(() =>{ Task("Test").IsDependentOn("Build").Does(() => { var settings = new DotNetTestSettings { Configuration = config, - Collectors = {"XPlat Code Coverage"} + Collectors = {"XPlat Code Coverage"}, + Loggers = {"console;verbosity=normal"} }; DotNetTest("MLEM.sln", settings); DotNetTest("MLEM.FNA.sln", settings); @@ -49,7 +52,7 @@ Task("Pack").IsDependentOn("Test").Does(() => { DotNetPack("MLEM.FNA.sln", settings); }); -Task("Push").WithCriteria(branch == "main" || branch == "release").IsDependentOn("Pack").Does(() => { +Task("Push").WithCriteria(branch == "main" || branch == "release", "Not on main or release branch").IsDependentOn("Pack").Does(() => { DotNetNuGetPushSettings settings; if (branch == "release") { settings = new DotNetNuGetPushSettings {