1
0
Fork 0
mirror of https://github.com/Ellpeck/MLEM.git synced 2024-11-24 13:38:34 +01:00

Compare commits

...

47 commits

Author SHA1 Message Date
Ell
443bb4d6c3 release 5.1.0 2021-09-24 16:51:06 +02:00
Ell
41b924ef34 Fixed rounding errors causing AutoInline elements to be pushed into the next line with some ui scales 2021-09-24 16:35:53 +02:00
Ell
a140e85300 fixed Docs casing in readme path 2021-09-24 04:50:42 +02:00
Ell
a53939837f added package readmes 2021-09-24 04:47:39 +02:00
Ell
e620ed0d87 don't allow adding a flag with value 0 using AddFlag 2021-09-22 21:32:34 +02:00
Ell
81dcbfb9a1 Fixed DynamicEnum AddFlag going into an infinite loop 2021-09-22 20:33:11 +02:00
Ell
1bbb12a1fa clarified auto-size exception in elements 2021-09-09 17:02:29 +02:00
Ell
9890c4895c Fixed tooltips not displaying correctly with auto-hiding paragraphs 2021-09-09 16:53:12 +02:00
Ell
05e320d4f4 don't use new caches for HasFlag and HasAnyFlag 2021-09-05 16:26:05 +02:00
Ell
54e3c98029 Improved DynamicEnum caching 2021-09-05 16:18:26 +02:00
Ell
6537ff00c1 Improved KeysExtensions memory usage 2021-08-27 20:36:04 +02:00
Ell
866dad49ab Use FontStashSharp's built-in LineHeight property for GenericStashFont 2021-08-19 21:43:17 +02:00
Ell
ff510c54c5 Fixed tiled NinePatches missing pixels with some scales 2021-08-06 22:40:32 +02:00
Ell
51833d523d Improved CopyExtensions construction speed 2021-08-05 03:59:14 +02:00
Ell
a9a7f2b421 Revert "(attempt to) reduce element sizing incompatibilities for auto-width/height elements"
This reverts commit 094de058c4.
2021-08-05 03:47:03 +02:00
Ell
094de058c4 (attempt to) reduce element sizing incompatibilities for auto-width/height elements 2021-08-05 03:40:47 +02:00
Ell
db7ee04d30 allow enumerating SoundEffectInstanceHandler entries 2021-08-02 20:34:13 +02:00
Ell
0c45f2d8e6 properly document DataTextureAtlas 2021-08-02 15:15:33 +02:00
Ell
e11eb459b8 only offset DataTextureAtlas pivot if it is specified 2021-07-31 04:04:04 +02:00
Ell
516265bf5b Fixed GenericFont's SplitString using incorrect width for special characters and improved documentation 2021-07-28 17:22:47 +02:00
Ell
57f8e56c38 Improved RawContentManager's reader loading and added better exception handling 2021-07-27 16:40:42 +02:00
Ell
8fac4a0b69 Allow adding Link children to non-Paragraph elements 2021-07-24 07:36:42 +02:00
Ell
01bec459de disallow creating Paragraphs without fonts 2021-07-22 04:51:41 +02:00
Ell
9eef1e5b1c added alt text and better gallery descriptions to README 2021-07-22 04:40:09 +02:00
Ell
bb9b322580 Fixed tooltips with custom text scale not snapping to the mouse correctly in their first displayed frame 2021-07-22 04:27:57 +02:00
Ell
e53d30e5ca propagate line height to bold and italic stash fonts 2021-07-20 03:09:17 +02:00
Ell
ebc6ec872b use a heuristic for GenericStashFont line height calculations and allow specifying a custom line height 2021-07-20 01:23:44 +02:00
Ell
527c4af3e4 updated dependencies 2021-07-20 00:55:36 +02:00
Ell
1067055bb5 code cleanup 2021-07-19 23:49:16 +02:00
Ell
a76c14b243 Adjusted GenericStashFont line height calculations to result in the same values as GenericSpriteFont 2021-07-19 23:10:27 +02:00
Ell
abac738123 Removed LINQ Any and All usage in various methods to improve memory usage 2021-07-18 22:18:46 +02:00
Ell
374d936be2 specify the MLEM version that Invert will be moved 2021-07-13 22:35:48 +02:00
Ell
a52b46dce9 Added ColorExtensions.Invert and made ColorHelper.Invert obsolete 2021-07-13 22:34:32 +02:00
Ell
6aa9ec03d4 Added customizable overloads for Keybind, Combination and GenericInput ToString methods 2021-07-13 15:41:42 +02:00
Ell
9a0b8ef846 cleaned up formatting 2021-07-12 03:16:19 +02:00
Ell
27fc5a74d9 added the ability to specify a coordinate offset in data texture atlases 2021-07-12 03:14:05 +02:00
Ell
ee2b0b82fe allow for RotateBy, RotateCw and RotateCcw to accept invalid directions again 2021-07-10 06:16:33 +02:00
Ell
bb189261d7 Added a masking character to text fields 2021-07-08 18:17:39 +02:00
Ell
8d92131630 improved Direction2 code style 2021-07-05 19:49:18 +02:00
Ell
f352e6b437 added Direction2Helper.RotateBy 2021-07-05 19:46:39 +02:00
Ell
d1b229b589 moved sound classes into /Sound 2021-07-05 16:36:48 +02:00
Ell
642608a8a2 Fixed a crash if a paragraph has a link formatting code, but no font 2021-07-03 01:50:37 +02:00
Ell
f71eb6eddb improved NinePatch memory performance 2021-07-03 01:44:39 +02:00
Ell
00d9ee99d8 Merge branch 'release' into main
# Conflicts:
#	CHANGELOG.md
2021-07-01 05:06:39 +02:00
Ell
579fd38533 very important formatting change in the changelog 2021-06-30 19:46:38 +02:00
Ell
58bd076e2a Set default values for InputHandler held and pressed keys to avoid an exception if buttons are held in the very first frame 2021-06-30 19:40:43 +02:00
Ell
25efa0bd50 bump upcoming version 2021-06-30 00:30:17 +02:00
48 changed files with 950 additions and 537 deletions

View file

@ -1,9 +1,60 @@
# Changelog
MLEM uses [semantic versioning](https://semver.org/).
MLEM tries to adhere to [semantic versioning](https://semver.org/).
Jump to version:
- [5.1.0](#510)
- [5.0.0](#500)
## 5.1.0
### MLEM
Additions
- Added RotateBy to Direction2Helper
Improvements
- Improved NinePatch memory usage
- Moved sound-related classes into Sound namespace
- Added customizable overloads for Keybind, Combination and GenericInput ToString methods
- Moved ColorHelper.Invert to ColorExtensions.Invert
- Removed LINQ Any and All usage in various methods to improve memory usage
- Allow enumerating SoundEffectInstanceHandler entries
- Improved KeysExtensions memory usage
Fixes
- Set default values for InputHandler held and pressed keys to avoid an exception if buttons are held in the very first frame
- Fixed GenericFont MeasureString using incorrect width for Zwsp and OneEmSpace
- Fixed tiled NinePatches missing pixels with some scales
### MLEM.Ui
Additions
- Added a masking character to TextField to allow for password-style text fields
Improvements
- Removed LINQ Any and All usage in various methods to improve memory usage
- Explicitly disallow creating Paragraphs without fonts to make starting out with MLEM.Ui less confusing
- Allow adding Link children to non-Paragraph elements
Fixes
- Fixed a crash if a paragraph has a link formatting code, but no font
- Fixed tooltips with custom text scale not snapping to the mouse correctly in their first displayed frame
- Fixed tooltips not displaying correctly with auto-hiding paragraphs
- Fixed rounding errors causing AutoInline elements to be pushed into the next line with some ui scales
### MLEM.Extended
Improvements
- Use FontStashSharp's built-in LineHeight property for GenericStashFont
### MLEM.Data
Additions
- Added the ability to specify a coordinate offset in data texture atlases
Improvements
- Improved RawContentManager's reader loading and added better exception handling
- Improved CopyExtensions construction speed
- Improved DynamicEnum caching
Fixes
- Fixed DynamicEnum AddFlag going into an infinite loop
## 5.0.0
### MLEM
Additions

View file

@ -8,6 +8,7 @@ using MLEM.Extensions;
using MLEM.Font;
using MLEM.Formatting;
using MLEM.Formatting.Codes;
using MLEM.Input;
using MLEM.Misc;
using MLEM.Startup;
using MLEM.Textures;
@ -48,7 +49,10 @@ namespace Demos {
RadioTexture = new NinePatch(new TextureRegion(this.testTexture, 16, 0, 8, 8), 3),
RadioCheckmark = new TextureRegion(this.testTexture, 32, 0, 8, 8)
};
var untexturedStyle = new UntexturedStyle(this.SpriteBatch);
var untexturedStyle = new UntexturedStyle(this.SpriteBatch) {
TextScale = style.TextScale,
Font = style.Font
};
// set the defined style as the current one
this.UiSystem.Style = style;
// scale every ui up by 5
@ -73,7 +77,7 @@ namespace Demos {
});
this.root.AddChild(new VerticalSpace(3));
this.root.AddChild(new Paragraph(Anchor.AutoLeft, 1, "Note that the default style does not contain any textures or font files and, as such, is quite bland. However, the default style is quite easy to override, as can be seen in the code for this demo."));
this.root.AddChild(new Paragraph(Anchor.AutoLeft, 1, "Note that the default style does not contain any textures and, as such, is quite bland. However, the default style is quite easy to override, as can be seen in the code for this demo."));
this.root.AddChild(new Button(Anchor.AutoCenter, new Vector2(1, 10), "Change Style") {
OnPressed = element => this.UiSystem.Style = this.UiSystem.Style == untexturedStyle ? style : untexturedStyle,
PositionOffset = new Vector2(0, 1),
@ -101,6 +105,13 @@ namespace Demos {
this.root.AddChild(new Paragraph(Anchor.AutoCenter, 1, "Numbers-only input:", true));
this.root.AddChild(new TextField(Anchor.AutoLeft, new Vector2(1, 10), TextField.OnlyNumbers) {PositionOffset = new Vector2(0, 1)});
this.root.AddChild(new VerticalSpace(3));
this.root.AddChild(new Paragraph(Anchor.AutoCenter, 1, "Password-style input:", true));
this.root.AddChild(new TextField(Anchor.AutoLeft, new Vector2(1, 10), text: "secret") {
PositionOffset = new Vector2(0, 1),
MaskingCharacter = '*'
});
this.root.AddChild(new VerticalSpace(3));
this.root.AddChild(new Paragraph(Anchor.AutoLeft, 1, "Zoom in or out:"));
this.root.AddChild(new Button(Anchor.AutoLeft, new Vector2(10), "+") {
@ -131,6 +142,10 @@ namespace Demos {
this.root.AddChild(new Button(Anchor.AutoLeft, new Vector2(1, 10), "Toggle Mouse Tooltip") {
OnPressed = element => tooltip.IsHidden = !tooltip.IsHidden
});
var delayed = this.root.AddChild(new Button(Anchor.AutoLeft, new Vector2(1, 10), "Delayed Tooltip") {PositionOffset = new Vector2(0, 1)});
delayed.AddTooltip(50, "This tooltip appears with a half second delay!").Delay = TimeSpan.FromSeconds(0.5);
var condition = this.root.AddChild(new Button(Anchor.AutoLeft, new Vector2(1, 10), "Hold Ctrl for Tooltip") {PositionOffset = new Vector2(0, 1)});
condition.AddTooltip(50, p => this.InputHandler.IsModifierKeyDown(ModifierKey.Control) ? "This tooltip only appears when holding control!" : string.Empty);
var slider = new Slider(Anchor.AutoLeft, new Vector2(1, 10), 5, 1) {
StepPerScroll = 0.01F
@ -178,7 +193,7 @@ namespace Demos {
CoroutineHandler.Start(WobbleProgressBar(bar2));
var bar3 = this.root.AddChild(new ProgressBar(Anchor.AutoLeft, new Vector2(8, 30), Direction2.Down, 10) {PositionOffset = new Vector2(0, 1)});
CoroutineHandler.Start(WobbleProgressBar(bar3));
var bar4 = this.root.AddChild(new ProgressBar(Anchor.AutoInline, new Vector2(8, 30), Direction2.Up, 10) {PositionOffset = new Vector2(1, 1)});
var bar4 = this.root.AddChild(new ProgressBar(Anchor.AutoInline, new Vector2(8, 30), Direction2.Up, 10) {PositionOffset = new Vector2(1, 0)});
CoroutineHandler.Start(WobbleProgressBar(bar4));
this.root.AddChild(new VerticalSpace(3));

View file

@ -1,4 +1,4 @@
<img src="../Media/Logo.svg" width="25%" >
![The MLEM logo](https://raw.githubusercontent.com/Ellpeck/MLEM/release/Media/Banner.png)
**MLEM Library for Extending MonoGame** is an addition to the game framework [MonoGame](https://www.monogame.net/) that provides extension methods, quality of life improvements and additional features like a ui system and easy input handling.
@ -6,9 +6,9 @@
- Get it on [NuGet](https://www.nuget.org/packages?q=mlem)
- Get prerelease builds on [BaGet](https://nuget.ellpeck.de)
- See the source code on [GitHub](https://github.com/Ellpeck/MLEM)
- See tutorials and API documentation on this website
- 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](../CHANGELOG.md) for information on updates
- See tutorials and API documentation on [the website](https://mlem.ellpeck.de/)
- Check out [the demos](https://github.com/Ellpeck/MLEM/tree/release/Demos) on [Desktop](https://github.com/Ellpeck/MLEM/tree/release/Demos.DesktopGL) or [Android](https://github.com/Ellpeck/MLEM/tree/release/Demos.Android)
- See [the changelog](https://mlem.ellpeck.de/CHANGELOG.html) for information on updates
# Made with MLEM
- [A Breath of Spring Air](https://ellpeck.itch.io/a-breath-of-spring-air), a short platformer ([Source](https://git.ellpeck.de/Ellpeck/GreatSpringGameJam))
@ -20,16 +20,17 @@ If you created a game with the help of MLEM, you can get it added to this list b
# Gallery
Here are some images that show a couple of MLEM's features.
MLEM.Ui in action:
<img src="../Media/Ui.gif">
The [MLEM.Ui](https://mlem.ellpeck.de/articles/ui) demo in action:
MLEM's text formatting system:
<img src="../Media/Formatting.png">
![A gif showing various user interface elements from the MLEM.Ui demo](https://raw.githubusercontent.com/Ellpeck/MLEM/release/Media/Ui.gif)
MLEM's [text formatting system](https://mlem.ellpeck.de/articles/text_formatting), which is compatible with both MLEM.Ui and regular sprite batch rendering:
![An image showing text with various colors and other formatting](https://raw.githubusercontent.com/Ellpeck/MLEM/release/Media/Formatting.png)
# Friends of MLEM
There are several other NuGet packages and tools that work well in combination with MonoGame and MLEM. Here are some of them:
- [Contentless](https://github.com/Ellpeck/Contentless), a tool that removes the need to add assets to the MonoGame Content Pipeline manually
- [GameBundle](https://github.com/Ellpeck/GameBundle), a tool that packages MonoGame and other .NET Core applications into several distributable formats
- [ButlerDotNet](https://github.com/Ellpeck/ButlerDotNet), a tool that automatically downloads and invokes itch.io's butler
- [MonoGame.Extended](https://github.com/craftworkgames/MonoGame.Extended), a package that also provides several additional features for MonoGame
- [Coroutine](https://github.com/Ellpeck/Coroutine), a package that implements Unity-style coroutines for any project

View file

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
@ -12,12 +11,7 @@ namespace MLEM.Data.Content {
/// </summary>
public class RawContentManager : ContentManager, IGameComponent {
private static readonly RawContentReader[] Readers = AppDomain.CurrentDomain.GetAssemblies()
.Where(a => !a.IsDynamic)
.SelectMany(a => a.GetExportedTypes())
.Where(t => t.IsSubclassOf(typeof(RawContentReader)) && !t.IsAbstract)
.Select(t => t.GetConstructor(Type.EmptyTypes).Invoke(null))
.Cast<RawContentReader>().ToArray();
private static List<RawContentReader> readers;
private readonly List<IDisposable> disposableAssets = new List<IDisposable>();
@ -57,7 +51,11 @@ namespace MLEM.Data.Content {
private T Read<T>(string assetName, T existing) {
var triedFiles = new List<string>();
foreach (var reader in Readers.Where(r => r.CanRead(typeof(T)))) {
if (readers == null)
readers = CollectContentReaders();
foreach (var reader in readers) {
if (!reader.CanRead(typeof(T)))
continue;
foreach (var ext in reader.GetFileExtensions()) {
var file = Path.Combine(this.RootDirectory, $"{assetName}.{ext}");
triedFiles.Add(file);
@ -89,5 +87,30 @@ namespace MLEM.Data.Content {
public void Initialize() {
}
private static List<RawContentReader> CollectContentReaders() {
var ret = new List<RawContentReader>();
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) {
try {
if (assembly.IsDynamic)
continue;
foreach (var type in assembly.GetExportedTypes()) {
try {
if (type.IsAbstract)
continue;
if (!type.IsSubclassOf(typeof(RawContentReader)))
continue;
var inst = type.GetConstructor(Type.EmptyTypes).Invoke(null);
ret.Add((RawContentReader) inst);
} catch (Exception e) {
throw new NotSupportedException($"The type {type} cannot be constructed by a RawContentManager. Does it have a visible parameterless constructor?", e);
}
}
} catch {
// ignored
}
}
return ret;
}
}
}

View file

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
@ -9,6 +10,7 @@ namespace MLEM.Data {
public static class CopyExtensions {
private const BindingFlags DefaultFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
private static readonly Dictionary<Type, ConstructorInfo> ConstructorCache = new Dictionary<Type, ConstructorInfo>();
/// <summary>
/// Creates a shallow copy of the object and returns it.
@ -85,17 +87,20 @@ namespace MLEM.Data {
}
private static object Construct(Type t, BindingFlags flags) {
var constructors = t.GetConstructors(flags);
// find a contructor with the correct attribute
var constructor = constructors.FirstOrDefault(c => c.GetCustomAttribute<CopyConstructorAttribute>() != null);
// find a parameterless construcotr
if (constructor == null)
constructor = t.GetConstructor(flags, null, Type.EmptyTypes, null);
// fall back to the first constructor
if (constructor == null)
constructor = constructors.FirstOrDefault();
if (constructor == null)
throw new NullReferenceException($"Type {t} does not have a constructor with the required visibility");
if (!ConstructorCache.TryGetValue(t, out var constructor)) {
var constructors = t.GetConstructors(flags);
// find a contructor with the correct attribute
constructor = constructors.FirstOrDefault(c => c.GetCustomAttribute<CopyConstructorAttribute>() != null);
// find a parameterless construcotr
if (constructor == null)
constructor = t.GetConstructor(flags, null, Type.EmptyTypes, null);
// fall back to the first constructor
if (constructor == null)
constructor = constructors.FirstOrDefault();
if (constructor == null)
throw new NullReferenceException($"Type {t} does not have a constructor with the required visibility");
ConstructorCache.Add(t, constructor);
}
return constructor.Invoke(new object[constructor.GetParameters().Length]);
}

View file

@ -9,11 +9,30 @@ using MLEM.Textures;
namespace MLEM.Data {
/// <summary>
/// <para>
/// This class represents an atlas of <see cref="TextureRegion"/> objects which are loaded from a special texture atlas file.
/// To load a data texture atlas, you can use <see cref="DataTextureAtlasExtensions.LoadTextureAtlas"/>.
/// To see the structure of a Data Texture Atlas, you can check out the sandbox project: <see href="https://github.com/Ellpeck/MLEM/blob/main/Sandbox/Content/Textures/Furniture.atlas"/>.
/// Additionally, if you are using Aseprite, there is a script to automatically populate it: <see href="https://github.com/Ellpeck/MLEM/blob/main/Utilities/Populate%20Data%20Texture%20Atlas.lua"/>
/// </para>
/// <para>
/// Data texture atlases are designed to be easy to write by hand. Because of this, their structure is very simple.
/// Each texture region defined in the atlas consists of its name, followed by a set of possible keywords and their arguments, separated by spaces.
/// The <c>loc</c> keyword defines the <see cref="TextureRegion.Area"/> of the texture region as a rectangle whose origin is its top-left corner. It requires four arguments: x, y, width and height of the rectangle.
/// The (optional) <c>piv</c> keyword defines the <see cref="TextureRegion.PivotPixels"/> of the texture region. It requires two arguments: x and y. If it is not supplied, the pivot defaults to the top-left corner of the texture region.
/// The (optional) <c>off</c> keyword defines an offset that is added onto the location and pivot of this texture region. This is useful when copying and pasting a previously defined texture region to create a second region that has a constant offset. It requires two arguments: The x and y offset.
/// </para>
/// <example>
/// The following entry defines a texture region with the name <c>LongTableRight</c>, whose <see cref="TextureRegion.Area"/> will be a rectangle with X=32, Y=30, Width=64, Height=48, and whose <see cref="TextureRegion.PivotPixels"/> will be a vector with X=80, Y=46.
/// <code>
/// LongTableRight
/// loc 32 30 64 48
/// piv 80 46
/// </code>
/// </example>
/// </summary>
/// <remarks>
/// To see a Data Texture Atlas in action, you can check out the sandbox project: <see href="https://github.com/Ellpeck/MLEM/blob/main/Sandbox/Content/Textures/Furniture.atlas"/>.
/// Additionally, if you are using Aseprite, there is a script to automatically populate it: <see href="https://github.com/Ellpeck/MLEM/blob/main/Utilities/Populate%20Data%20Texture%20Atlas.lua"/>.
/// </remarks>
public class DataTextureAtlas {
/// <summary>
@ -56,32 +75,29 @@ namespace MLEM.Data {
text = reader.ReadToEnd();
var atlas = new DataTextureAtlas(texture);
// parse each texture region: "<name> loc <u> <v> <w> <h> piv <px> <py>" followed by extra data in the form "key <x> <y>"
foreach (Match match in Regex.Matches(text, @"(.+)\W+loc\W+([0-9]+)\W+([0-9]+)\W+([0-9]+)\W+([0-9]+)(?:\W+piv\W+([0-9.]+)\W+([0-9.]+))?(?:\W+(\w+)\W+([0-9.]+)\W+([0-9.]+))*")) {
// parse each texture region: "<name> loc <u> <v> <w> <h> [piv <px> <py>] [off <ox> <oy>]"
foreach (Match match in Regex.Matches(text, @"(.+)\W+loc\W+([0-9]+)\W+([0-9]+)\W+([0-9]+)\W+([0-9]+)\W*(?:piv\W+([0-9.]+)\W+([0-9.]+))?\W*(?:off\W+([0-9.]+)\W+([0-9.]+))?")) {
var name = match.Groups[1].Value.Trim();
// offset
var off = !match.Groups[8].Success ? Vector2.Zero : new Vector2(
float.Parse(match.Groups[8].Value, CultureInfo.InvariantCulture),
float.Parse(match.Groups[9].Value, CultureInfo.InvariantCulture));
// location
var loc = new Rectangle(
int.Parse(match.Groups[2].Value), int.Parse(match.Groups[3].Value),
int.Parse(match.Groups[4].Value), int.Parse(match.Groups[5].Value));
loc.Offset(off);
// pivot
var piv = Vector2.Zero;
if (match.Groups[6].Success) {
piv = new Vector2(
float.Parse(match.Groups[6].Value, CultureInfo.InvariantCulture) - (pivotRelative ? 0 : loc.X),
float.Parse(match.Groups[7].Value, CultureInfo.InvariantCulture) - (pivotRelative ? 0 : loc.Y));
}
var piv = !match.Groups[6].Success ? Vector2.Zero : off + new Vector2(
float.Parse(match.Groups[6].Value, CultureInfo.InvariantCulture) - (pivotRelative ? 0 : loc.X),
float.Parse(match.Groups[7].Value, CultureInfo.InvariantCulture) - (pivotRelative ? 0 : loc.Y));
var region = new TextureRegion(texture, loc) {
PivotPixels = piv,
Name = name
};
// additional data
if (match.Groups[8].Success) {
for (var i = 0; i < match.Groups[8].Captures.Count; i++) {
region.SetData(match.Groups[8].Captures[i].Value, new Vector2(
float.Parse(match.Groups[9].Captures[i].Value, CultureInfo.InvariantCulture) - (pivotRelative ? 0 : loc.X),
float.Parse(match.Groups[10].Captures[i].Value, CultureInfo.InvariantCulture) - (pivotRelative ? 0 : loc.Y)));
}
}
atlas.regions.Add(name, region);
}

View file

@ -12,6 +12,9 @@ namespace MLEM.Data {
/// A dynamic enum uses <see cref="BigInteger"/> as its underlying type, allowing for an arbitrary number of enum values to be created, even when a <see cref="FlagsAttribute"/>-like structure is used that would only allow for up to 64 values in a regular enum.
/// All enum operations including <see cref="And{T}"/>, <see cref="Or{T}"/>, <see cref="Xor{T}"/> and <see cref="Neg{T}"/> are supported and can be implemented in derived classes using operator overloads.
/// To create a custom dynamic enum, simply create a class that extends <see cref="DynamicEnum"/>. New values can then be added using <see cref="Add{T}"/>, <see cref="AddValue{T}"/> or <see cref="AddFlag{T}"/>.
///
/// This class, and its entire concept, are extremely terrible. If you intend on using this, there's probably at least one better solution available.
/// Though if, for some weird reason, you need a way to have more than 64 distinct flags, this is a pretty good solution.
/// </summary>
/// <remarks>
/// To include enum-like operator overloads in a dynamic enum named MyEnum, the following code can be used:
@ -27,10 +30,7 @@ namespace MLEM.Data {
[JsonConverter(typeof(DynamicEnumConverter))]
public abstract class DynamicEnum {
private static readonly Dictionary<Type, Dictionary<BigInteger, DynamicEnum>> Values = new Dictionary<Type, Dictionary<BigInteger, DynamicEnum>>();
private static readonly Dictionary<Type, Dictionary<BigInteger, DynamicEnum>> FlagCache = new Dictionary<Type, Dictionary<BigInteger, DynamicEnum>>();
private static readonly Dictionary<Type, Dictionary<string, DynamicEnum>> ParseCache = new Dictionary<Type, Dictionary<string, DynamicEnum>>();
private static readonly Dictionary<Type, Storage> Storages = new Dictionary<Type, Storage>();
private readonly BigInteger value;
private Dictionary<DynamicEnum, bool> allFlagsCache;
@ -59,7 +59,6 @@ namespace MLEM.Data {
if (this.allFlagsCache == null)
this.allFlagsCache = new Dictionary<DynamicEnum, bool>();
if (!this.allFlagsCache.TryGetValue(flags, out var ret)) {
// & is very memory-intensive, so we cache the return value
ret = (GetValue(this) & GetValue(flags)) == GetValue(flags);
this.allFlagsCache.Add(flags, ret);
}
@ -76,7 +75,6 @@ namespace MLEM.Data {
if (this.anyFlagsCache == null)
this.anyFlagsCache = new Dictionary<DynamicEnum, bool>();
if (!this.anyFlagsCache.TryGetValue(flags, out var ret)) {
// & is very memory-intensive, so we cache the return value
ret = (GetValue(this) & GetValue(flags)) != 0;
this.anyFlagsCache.Add(flags, ret);
}
@ -107,22 +105,20 @@ namespace MLEM.Data {
/// <returns>The newly created enum value</returns>
/// <exception cref="ArgumentException">Thrown if the name or value passed are already present</exception>
public static T Add<T>(string name, BigInteger value) where T : DynamicEnum {
if (!Values.TryGetValue(typeof(T), out var dict)) {
dict = new Dictionary<BigInteger, DynamicEnum>();
Values.Add(typeof(T), dict);
}
var storage = GetStorage(typeof(T));
// cached parsed values and names might be incomplete with new values
FlagCache.Remove(typeof(T));
ParseCache.Remove(typeof(T));
storage.ClearCaches();
if (dict.ContainsKey(value))
if (storage.Values.ContainsKey(value))
throw new ArgumentException($"Duplicate value {value}", nameof(value));
if (dict.Values.Any(v => v.name == name))
throw new ArgumentException($"Duplicate name {name}", nameof(name));
foreach (var v in storage.Values.Values) {
if (v.name == name)
throw new ArgumentException($"Duplicate name {name}", nameof(name));
}
var ret = Construct(typeof(T), name, value);
dict.Add(value, ret);
storage.Values.Add(value, ret);
return (T) ret;
}
@ -136,10 +132,8 @@ namespace MLEM.Data {
/// <returns>The newly created enum value</returns>
public static T AddValue<T>(string name) where T : DynamicEnum {
BigInteger value = 0;
if (Values.TryGetValue(typeof(T), out var defined)) {
while (defined.ContainsKey(value))
value++;
}
while (GetStorage(typeof(T)).Values.ContainsKey(value))
value++;
return Add<T>(name, value);
}
@ -152,11 +146,9 @@ namespace MLEM.Data {
/// <typeparam name="T">The type to add this value to</typeparam>
/// <returns>The newly created enum value</returns>
public static T AddFlag<T>(string name) where T : DynamicEnum {
BigInteger value = 0;
if (Values.TryGetValue(typeof(T), out var defined)) {
while (defined.ContainsKey(value))
value <<= 1;
}
BigInteger value = 1;
while (GetStorage(typeof(T)).Values.ContainsKey(value))
value <<= 1;
return Add<T>(name, value);
}
@ -177,7 +169,7 @@ namespace MLEM.Data {
/// <param name="type">The type whose values to get</param>
/// <returns>The defined values for the given type</returns>
public static IEnumerable<DynamicEnum> GetValues(Type type) {
return Values.TryGetValue(type, out var ret) ? ret.Values : Enumerable.Empty<DynamicEnum>();
return GetStorage(type).Values.Values;
}
/// <summary>
@ -188,7 +180,12 @@ namespace MLEM.Data {
/// <typeparam name="T">The type of the values</typeparam>
/// <returns>The bitwise OR (|) combination</returns>
public static T Or<T>(T left, T right) where T : DynamicEnum {
return GetEnumValue<T>(GetValue(left) | GetValue(right));
var cache = GetStorage(typeof(T)).OrCache;
if (!cache.TryGetValue((left, right), out var ret)) {
ret = GetEnumValue<T>(GetValue(left) | GetValue(right));
cache.Add((left, right), ret);
}
return (T) ret;
}
/// <summary>
@ -199,7 +196,12 @@ namespace MLEM.Data {
/// <typeparam name="T">The type of the values</typeparam>
/// <returns>The bitwise AND (&amp;) combination</returns>
public static T And<T>(T left, T right) where T : DynamicEnum {
return GetEnumValue<T>(GetValue(left) & GetValue(right));
var cache = GetStorage(typeof(T)).AndCache;
if (!cache.TryGetValue((left, right), out var ret)) {
ret = GetEnumValue<T>(GetValue(left) & GetValue(right));
cache.Add((left, right), ret);
}
return (T) ret;
}
/// <summary>
@ -210,7 +212,12 @@ namespace MLEM.Data {
/// <typeparam name="T">The type of the values</typeparam>
/// <returns>The bitwise XOR (^) combination</returns>
public static T Xor<T>(T left, T right) where T : DynamicEnum {
return GetEnumValue<T>(GetValue(left) ^ GetValue(right));
var cache = GetStorage(typeof(T)).XorCache;
if (!cache.TryGetValue((left, right), out var ret)) {
ret = GetEnumValue<T>(GetValue(left) ^ GetValue(right));
cache.Add((left, right), ret);
}
return (T) ret;
}
/// <summary>
@ -220,7 +227,12 @@ namespace MLEM.Data {
/// <typeparam name="T">The type of the values</typeparam>
/// <returns>The bitwise NEG (~) value</returns>
public static T Neg<T>(T value) where T : DynamicEnum {
return GetEnumValue<T>(~GetValue(value));
var cache = GetStorage(typeof(T)).NegCache;
if (!cache.TryGetValue(value, out var ret)) {
ret = GetEnumValue<T>(~GetValue(value));
cache.Add(value, ret);
}
return (T) ret;
}
/// <summary>
@ -249,18 +261,16 @@ namespace MLEM.Data {
/// <param name="value">The value whose dynamic enum value to get</param>
/// <returns>The defined or combined dynamic enum value</returns>
public static DynamicEnum GetEnumValue(Type type, BigInteger value) {
var storage = GetStorage(type);
// get the defined value if it exists
if (Values.TryGetValue(type, out var values) && values.TryGetValue(value, out var defined))
if (storage.Values.TryGetValue(value, out var defined))
return defined;
// otherwise, cache the combined value
if (!FlagCache.TryGetValue(type, out var cache)) {
cache = new Dictionary<BigInteger, DynamicEnum>();
FlagCache.Add(type, cache);
}
if (!cache.TryGetValue(value, out var combined)) {
if (!storage.FlagCache.TryGetValue(value, out var combined)) {
combined = Construct(type, null, value);
cache.Add(value, combined);
storage.FlagCache.Add(value, combined);
}
return combined;
}
@ -285,10 +295,7 @@ namespace MLEM.Data {
/// <param name="strg">The string to parse into a dynamic enum value</param>
/// <returns>The parsed enum value, or null if parsing fails</returns>
public static DynamicEnum Parse(Type type, string strg) {
if (!ParseCache.TryGetValue(type, out var cache)) {
cache = new Dictionary<string, DynamicEnum>();
ParseCache.Add(type, cache);
}
var cache = GetStorage(type).ParseCache;
if (!cache.TryGetValue(strg, out var cached)) {
BigInteger? accum = null;
foreach (var val in strg.Split('|')) {
@ -306,10 +313,39 @@ namespace MLEM.Data {
return cached;
}
private static Storage GetStorage(Type type) {
if (!Storages.TryGetValue(type, out var storage)) {
storage = new Storage();
Storages.Add(type, storage);
}
return storage;
}
private static DynamicEnum Construct(Type type, string name, BigInteger value) {
var constructor = type.GetConstructor(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new[] {typeof(string), typeof(BigInteger)}, null);
return (DynamicEnum) constructor.Invoke(new object[] {name, value});
}
private class Storage {
public readonly Dictionary<BigInteger, DynamicEnum> Values = new Dictionary<BigInteger, DynamicEnum>();
public readonly Dictionary<BigInteger, DynamicEnum> FlagCache = new Dictionary<BigInteger, DynamicEnum>();
public readonly Dictionary<string, DynamicEnum> ParseCache = new Dictionary<string, DynamicEnum>();
public readonly Dictionary<(DynamicEnum, DynamicEnum), DynamicEnum> OrCache = new Dictionary<(DynamicEnum, DynamicEnum), DynamicEnum>();
public readonly Dictionary<(DynamicEnum, DynamicEnum), DynamicEnum> AndCache = new Dictionary<(DynamicEnum, DynamicEnum), DynamicEnum>();
public readonly Dictionary<(DynamicEnum, DynamicEnum), DynamicEnum> XorCache = new Dictionary<(DynamicEnum, DynamicEnum), DynamicEnum>();
public readonly Dictionary<DynamicEnum, DynamicEnum> NegCache = new Dictionary<DynamicEnum, DynamicEnum>();
public void ClearCaches() {
this.FlagCache.Clear();
this.ParseCache.Clear();
this.OrCache.Clear();
this.AndCache.Clear();
this.XorCache.Clear();
this.NegCache.Clear();
}
}
}
}

View file

@ -13,6 +13,7 @@
<RepositoryUrl>https://github.com/Ellpeck/MLEM</RepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageIcon>Logo.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>
<ItemGroup>
@ -32,5 +33,6 @@
<ItemGroup>
<None Include="../Media/Logo.png" Pack="true" PackagePath="" />
<None Include="../Docs/index.md" Pack="true" PackagePath="README.md" />
</ItemGroup>
</Project>

View file

@ -33,9 +33,9 @@ namespace MLEM.Extended.Font {
}
/// <inheritdoc />
protected override Vector2 MeasureChar(char c) {
protected override float MeasureChar(char c) {
var region = this.Font.GetCharacterRegion(c);
return region != null ? new Vector2(region.XAdvance, region.Height) : Vector2.Zero;
return region != null ? new Vector2(region.XAdvance, region.Height).X : 0;
}
/// <inheritdoc/>

View file

@ -18,7 +18,7 @@ namespace MLEM.Extended.Font {
/// <inheritdoc />
public override GenericFont Italic { get; }
/// <inheritdoc />
public override float LineHeight { get; }
public override float LineHeight => this.Font.LineHeight;
/// <summary>
/// Creates a new generic font using <see cref="SpriteFontBase"/>.
@ -29,9 +29,6 @@ namespace MLEM.Extended.Font {
/// <param name="italic">An italic version of the font</param>
public GenericStashFont(SpriteFontBase font, SpriteFontBase bold = null, SpriteFontBase italic = null) {
this.Font = font;
// SpriteFontBase provides no line height, so we measure the height of a new line for most fonts
// This doesn't work with static sprite fonts, but their size is always the one we calculate here
this.LineHeight = font is StaticSpriteFont s ? s.FontSize + s.LineSpacing : font.MeasureString("\n").Y;
this.Bold = bold != null ? new GenericStashFont(bold) : this;
this.Italic = italic != null ? new GenericStashFont(italic) : this;
}
@ -47,8 +44,8 @@ namespace MLEM.Extended.Font {
}
/// <inheritdoc />
protected override Vector2 MeasureChar(char c) {
return this.Font.MeasureString(c.ToCachedString());
protected override float MeasureChar(char c) {
return this.Font.MeasureString(c.ToCachedString()).X;
}
}

View file

@ -13,6 +13,7 @@
<RepositoryUrl>https://github.com/Ellpeck/MLEM</RepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageIcon>Logo.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>
<ItemGroup>
@ -24,7 +25,7 @@
<PackageReference Include="MonoGame.Extended.Tiled" Version="3.8.0">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="FontStashSharp.MonoGame" Version="0.9.2">
<PackageReference Include="FontStashSharp.MonoGame" Version="1.0.3">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.0.1641">
@ -33,6 +34,7 @@
</ItemGroup>
<ItemGroup>
<None Include="../Media/Logo.png" Pack="true" PackagePath=""/>
<None Include="../Media/Logo.png" Pack="true" PackagePath="" />
<None Include="../Docs/index.md" Pack="true" PackagePath="README.md" />
</ItemGroup>
</Project>

View file

@ -72,7 +72,15 @@ namespace MLEM.Extended.Tiled {
/// <param name="included">A function that determines if a certain info should be included or not</param>
/// <returns>An enumerable of collision infos for that area</returns>
public IEnumerable<TileCollisionInfo> GetCollidingTiles(RectangleF area, Func<TileCollisionInfo, bool> included = null) {
var inclusionFunc = included ?? (tile => tile.Collisions.Any(c => c.Intersects(area)));
bool DefaultInclusion(TileCollisionInfo tile) {
foreach (var c in tile.Collisions) {
if (c.Intersects(area))
return true;
}
return false;
}
var inclusionFunc = included ?? DefaultInclusion;
var minX = Math.Max(0, area.Left.Floor());
var maxX = Math.Min(this.map.Width - 1, area.Right.Floor());
var minY = Math.Max(0, area.Top.Floor());

View file

@ -14,6 +14,7 @@
<RepositoryUrl>https://github.com/Ellpeck/MLEM</RepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageIcon>Logo.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>
<ItemGroup>
@ -28,5 +29,6 @@
<ItemGroup>
<None Include="../Media/Logo.png" Pack="true" PackagePath="" />
<None Include="../Docs/index.md" Pack="true" PackagePath="README.md" />
</ItemGroup>
</Project>

View file

@ -19,12 +19,14 @@
<PackageProjectUrl>https://mlem.ellpeck.de/</PackageProjectUrl>
<RepositoryUrl>https://github.com/Ellpeck/MLEM</RepositoryUrl>
<PackageIcon>Logo.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>
<ItemGroup>
<Content Include="content\**\*" Exclude="content\**\.DS_Store;content\**\bin;content\**\obj" />
<Compile Remove="**\*" />
<None Include="../Media/Logo.png" Pack="true" PackagePath=""/>
<None Include="../Docs/index.md" Pack="true" PackagePath="README.md" />
</ItemGroup>
</Project>

View file

@ -10,6 +10,7 @@ using MLEM.Input;
using MLEM.Misc;
using MLEM.Textures;
using MLEM.Ui.Style;
using SoundEffectInfo = MLEM.Sound.SoundEffectInfo;
namespace MLEM.Ui.Elements {
/// <summary>
@ -606,7 +607,9 @@ namespace MLEM.Ui.Elements {
break;
case Anchor.AutoInline:
var newX = prevArea.Right + this.ScaledOffset.X;
if (newX + newSize.X <= parentArea.Right) {
// with awkward ui scale values, floating point rounding can cause an element that would usually be
// positioned correctly to be pushed into the next line due to a very small deviation, so we add 0.01 here
if (newX + newSize.X <= parentArea.Right + 0.01F) {
pos.X = newX;
pos.Y = prevArea.Y + this.ScaledOffset.Y;
} else {
@ -648,8 +651,8 @@ namespace MLEM.Ui.Elements {
autoSize.Y = lowest.UnscrolledArea.Bottom - pos.Y + this.ScaledChildPadding.Bottom;
foundChild = lowest;
} else {
if (this.Children.Count > 0)
throw new InvalidOperationException($"{this} with root {this.Root.Name} sets its height based on children but it only has children anchored too low ({string.Join(", ", this.Children.Select(c => c.Anchor))})");
if (this.Children.Any(e => !e.IsHidden))
throw new InvalidOperationException($"{this} with root {this.Root.Name} sets its height based on children but it only has visible children anchored too low ({string.Join(", ", this.Children.Where(c => !c.IsHidden).Select(c => c.Anchor))})");
autoSize.Y = 0;
}
}
@ -660,8 +663,8 @@ namespace MLEM.Ui.Elements {
autoSize.X = rightmost.UnscrolledArea.Right - pos.X + this.ScaledChildPadding.Right;
foundChild = rightmost;
} else {
if (this.Children.Count > 0)
throw new InvalidOperationException($"{this} with root {this.Root.Name} sets its width based on children but it only has children anchored too far right ({string.Join(", ", this.Children.Select(c => c.Anchor))})");
if (this.Children.Any(e => !e.IsHidden))
throw new InvalidOperationException($"{this} with root {this.Root.Name} sets its width based on children but it only has visible children anchored too far right ({string.Join(", ", this.Children.Where(c => !c.IsHidden).Select(c => c.Anchor))})");
autoSize.X = 0;
}
}
@ -672,6 +675,7 @@ namespace MLEM.Ui.Elements {
autoSize = Vector2.Min(autoSize, actualSize);
}
// we want to leave some leeway to prevent float rounding causing an infinite loop
if (!autoSize.Equals(this.UnscrolledArea.Size, 0.01F)) {
recursion++;
if (recursion >= 16) {

View file

@ -131,11 +131,7 @@ namespace MLEM.Ui.Elements {
public static Button KeybindButton(Anchor anchor, Vector2 size, Keybind keybind, InputHandler inputHandler, string activePlaceholder, GenericInput unbindKey = default, string unboundPlaceholder = "", Func<GenericInput, string> inputName = null) {
string GetCurrentName() {
var combination = keybind.GetCombinations().FirstOrDefault();
if (combination == null)
return unboundPlaceholder;
return string.Join(" + ", combination.Modifiers
.Append(combination.Key)
.Select(i => inputName?.Invoke(i) ?? i.ToString()));
return combination?.ToString(" + ", inputName) ?? unboundPlaceholder;
}
var button = new Button(anchor, size, GetCurrentName());

View file

@ -151,8 +151,8 @@ namespace MLEM.Ui.Elements {
/// <inheritdoc />
protected override void InitStyle(UiStyle style) {
base.InitStyle(style);
this.RegularFont.SetFromStyle(style.Font ?? throw new NotSupportedException("Paragraphs cannot use ui styles that don't have a font. Please supply a custom font by setting UiStyle.Font."));
this.TextScale.SetFromStyle(style.TextScale);
this.RegularFont.SetFromStyle(style.Font);
this.TextColor.SetFromStyle(style.TextColor);
}
@ -236,8 +236,11 @@ namespace MLEM.Ui.Elements {
/// <inheritdoc />
public override void ForceUpdateArea() {
// set the position offset and size to the token's first area
var area = this.Token.GetArea(Vector2.Zero, this.textScale).First();
this.PositionOffset = area.Location + new Vector2(((Paragraph) this.Parent).GetAlignmentOffset() / this.Parent.Scale, 0);
var area = this.Token.GetArea(Vector2.Zero, this.textScale).FirstOrDefault();
if (this.Parent is Paragraph p)
area.Location += new Vector2(p.GetAlignmentOffset() / p.Scale, 0);
this.PositionOffset = area.Location;
this.IsHidden = area.IsEmpty;
this.Size = area.Size;
base.ForceUpdateArea();
}

View file

@ -1,6 +1,5 @@
using System;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
@ -24,19 +23,43 @@ namespace MLEM.Ui.Elements {
/// <summary>
/// A <see cref="Rule"/> that allows any visible character and spaces
/// </summary>
public static readonly Rule DefaultRule = (field, add) => !add.Any(char.IsControl);
public static readonly Rule DefaultRule = (field, add) => {
foreach (var c in add) {
if (char.IsControl(c))
return false;
}
return true;
};
/// <summary>
/// A <see cref="Rule"/> that only allows letters
/// </summary>
public static readonly Rule OnlyLetters = (field, add) => add.All(char.IsLetter);
public static readonly Rule OnlyLetters = (field, add) => {
foreach (var c in add) {
if (!char.IsLetter(c))
return false;
}
return true;
};
/// <summary>
/// A <see cref="Rule"/> that only allows numerals
/// </summary>
public static readonly Rule OnlyNumbers = (field, add) => add.All(char.IsNumber);
public static readonly Rule OnlyNumbers = (field, add) => {
foreach (var c in add) {
if (!char.IsNumber(c))
return false;
}
return true;
};
/// <summary>
/// A <see cref="Rule"/> that only allows letters and numerals
/// </summary>
public static readonly Rule LettersNumbers = (field, add) => add.All(c => char.IsLetter(c) || char.IsNumber(c));
public static readonly Rule LettersNumbers = (field, add) => {
foreach (var c in add) {
if (!char.IsLetter(c) || !char.IsNumber(c))
return false;
}
return true;
};
/// <summary>
/// A <see cref="Rule"/> that only allows characters not contained in <see cref="Path.GetInvalidPathChars"/>
/// </summary>
@ -74,7 +97,6 @@ namespace MLEM.Ui.Elements {
/// The font that this text field should display text with
/// </summary>
public StyleProp<GenericFont> Font;
private readonly StringBuilder text = new StringBuilder();
/// <summary>
/// This text field's current text
/// </summary>
@ -95,9 +117,6 @@ namespace MLEM.Ui.Elements {
/// The width that the caret should render with.
/// </summary>
public float CaretWidth = 0.5F;
private double caretBlinkTimer;
private string displayedText;
private int textOffset;
/// <summary>
/// The rule used for text input.
/// Rules allow only certain characters to be allowed inside of a text field.
@ -111,7 +130,6 @@ namespace MLEM.Ui.Elements {
/// The description of the <c>KeyboardInput</c> field on mobile devices and consoles
/// </summary>
public string MobileDescription;
private int caretPos;
/// <summary>
/// The position of the caret within the text.
/// This is always between 0 and the <see cref="string.Length"/> of <see cref="Text"/>
@ -128,6 +146,26 @@ namespace MLEM.Ui.Elements {
}
}
}
/// <summary>
/// A character that should be displayed instead of this text field's <see cref="Text"/> content.
/// The amount of masking characters displayed will be equal to the <see cref="Text"/>'s length.
/// This behavior is useful for password fields or similar.
/// </summary>
public char? MaskingCharacter {
get => this.maskingCharacter;
set {
this.maskingCharacter = value;
this.HandleTextChange(false);
}
}
private readonly StringBuilder text = new StringBuilder();
private char? maskingCharacter;
private double caretBlinkTimer;
private string displayedText;
private int textOffset;
private int caretPos;
/// <summary>
/// Creates a new text field with the given settings
@ -136,18 +174,17 @@ namespace MLEM.Ui.Elements {
/// <param name="size">The text field's size</param>
/// <param name="rule">The text field's input rule</param>
/// <param name="font">The font to use for drawing text</param>
public TextField(Anchor anchor, Vector2 size, Rule rule = null, GenericFont font = null) : base(anchor, size) {
/// <param name="text">The text that the text field should contain by default</param>
public TextField(Anchor anchor, Vector2 size, Rule rule = null, GenericFont font = null, string text = null) : base(anchor, size) {
this.InputRule = rule ?? DefaultRule;
if (font != null)
this.Font.Set(font);
if (text != null)
this.SetText(text, true);
MlemPlatform.EnsureExists();
this.OnPressed += async e => {
var title = this.MobileTitle ?? this.PlaceholderText;
var result = await MlemPlatform.Current.OpenOnScreenKeyboard(title, this.MobileDescription, this.Text, false);
if (result != null)
this.SetText(result.Replace('\n', ' '), true);
};
this.OnPressed += OnPressed;
this.OnTextInput += (element, key, character) => {
if (!this.IsSelected || this.IsHidden)
return;
@ -164,6 +201,13 @@ namespace MLEM.Ui.Elements {
};
this.OnDeselected += e => this.CaretPos = 0;
this.OnSelected += e => this.CaretPos = this.text.Length;
async void OnPressed(Element e) {
var title = this.MobileTitle ?? this.PlaceholderText;
var result = await MlemPlatform.Current.OpenOnScreenKeyboard(title, this.MobileDescription, this.Text, false);
if (result != null)
this.SetText(result.Replace('\n', ' '), true);
}
}
private void HandleTextChange(bool textChanged = true) {
@ -190,6 +234,8 @@ namespace MLEM.Ui.Elements {
this.displayedText = this.Text;
this.textOffset = 0;
}
if (this.MaskingCharacter != null)
this.displayedText = new string(this.MaskingCharacter.Value, this.displayedText.Length);
if (textChanged)
this.OnTextChange?.Invoke(this, this.Text);

View file

@ -1,5 +1,4 @@
using System;
using System.Linq;
using Microsoft.Xna.Framework;
using MLEM.Ui.Style;
@ -25,6 +24,7 @@ namespace MLEM.Ui.Elements {
public Paragraph Paragraph;
private TimeSpan delayCountdown;
private bool autoHidden;
/// <summary>
/// Creates a new tooltip with the given settings
@ -56,10 +56,14 @@ namespace MLEM.Ui.Elements {
base.Update(time);
this.SnapPositionToMouse();
if (this.IsHidden && this.delayCountdown > TimeSpan.Zero) {
if (this.delayCountdown > TimeSpan.Zero) {
this.delayCountdown -= time.ElapsedGameTime;
if (this.delayCountdown <= TimeSpan.Zero)
if (this.delayCountdown <= TimeSpan.Zero) {
this.IsHidden = false;
this.UpdateAutoHidden();
}
} else {
this.UpdateAutoHidden();
}
}
@ -68,6 +72,7 @@ namespace MLEM.Ui.Elements {
if (this.Parent != null)
throw new NotSupportedException($"A tooltip shouldn't be the child of another element ({this.Parent})");
base.ForceUpdateArea();
this.SnapPositionToMouse();
}
/// <inheritdoc />
@ -111,6 +116,7 @@ namespace MLEM.Ui.Elements {
this.IsHidden = true;
this.delayCountdown = this.Delay;
}
this.autoHidden = false;
}
/// <summary>
@ -127,11 +133,7 @@ namespace MLEM.Ui.Elements {
/// </summary>
/// <param name="elementToHover">The element that should automatically cause the tooltip to appear and disappear when hovered and not hovered, respectively</param>
public void AddToElement(Element elementToHover) {
elementToHover.OnMouseEnter += element => {
// only display the tooltip if there is anything in it
if (this.Children.Any(c => !c.IsHidden))
this.Display(element.System, element.GetType().Name + "Tooltip");
};
elementToHover.OnMouseEnter += element => this.Display(element.System, element.GetType().Name + "Tooltip");
elementToHover.OnMouseExit += element => this.Remove();
}
@ -148,5 +150,21 @@ namespace MLEM.Ui.Elements {
this.AddToElement(elementToHover);
}
private void UpdateAutoHidden() {
var shouldBeHidden = true;
foreach (var child in this.Children) {
if (!child.IsHidden) {
shouldBeHidden = false;
break;
}
}
if (this.autoHidden != shouldBeHidden) {
// only auto-hide if IsHidden wasn't changed manually
if (this.IsHidden == this.autoHidden)
this.IsHidden = shouldBeHidden;
this.autoHidden = shouldBeHidden;
}
}
}
}

View file

@ -13,10 +13,11 @@
<RepositoryUrl>https://github.com/Ellpeck/MLEM</RepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageIcon>Logo.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="TextCopy" Version="4.3.0" />
<PackageReference Include="TextCopy" Version="4.3.1" />
<ProjectReference Include="..\MLEM\MLEM.csproj" />
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.0.1641">
@ -25,6 +26,7 @@
</ItemGroup>
<ItemGroup>
<None Include="../Media/Logo.png" Pack="true" PackagePath=""/>
<None Include="../Media/Logo.png" Pack="true" PackagePath="" />
<None Include="../Docs/index.md" Pack="true" PackagePath="README.md" />
</ItemGroup>
</Project>

View file

@ -5,6 +5,7 @@ using MLEM.Formatting;
using MLEM.Misc;
using MLEM.Textures;
using MLEM.Ui.Elements;
using SoundEffectInfo = MLEM.Sound.SoundEffectInfo;
namespace MLEM.Ui.Style {
/// <summary>

View file

@ -1,8 +1,6 @@
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using MLEM.Extensions;
using MLEM.Font;
namespace MLEM.Ui.Style {
/// <summary>
@ -37,25 +35,6 @@ namespace MLEM.Ui.Style {
this.ProgressBarColor = Color.White;
this.ProgressBarProgressPadding = new Vector2(1);
this.ProgressBarProgressColor = Color.Red;
this.Font = new EmptyFont();
}
private class EmptyFont : GenericFont {
public override GenericFont Bold => this;
public override GenericFont Italic => this;
public override float LineHeight => 1;
protected override Vector2 MeasureChar(char c) {
return Vector2.Zero;
}
public override void DrawString(SpriteBatch batch, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth) {
}
public override void DrawString(SpriteBatch batch, StringBuilder text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth) {
}
}
}

View file

@ -465,8 +465,14 @@ namespace MLEM.Ui {
this.CanSelectContent = true;
};
this.OnElementRemoved += e => {
if (e.CanBeSelected && !this.Element.GetChildren(regardGrandchildren: true).Any(c => c.CanBeSelected))
if (e.CanBeSelected) {
// check if removing this element removed all other selectable elements
foreach (var c in this.Element.GetChildren(regardGrandchildren: true)) {
if (c.CanBeSelected)
return;
}
this.CanSelectContent = false;
}
};
}

View file

@ -18,15 +18,8 @@ namespace MLEM.Extensions {
return color * (other.A / 255F);
}
}
/// <summary>
/// A set of utility methods for dealing with <see cref="Color"/> objects
/// </summary>
public static class ColorHelper {
/// <summary>
/// Returns an inverted version of the color.
/// Returns an inverted version of this color.
/// </summary>
/// <param name="color">The color to invert</param>
/// <returns>The inverted color</returns>
@ -34,6 +27,13 @@ namespace MLEM.Extensions {
return new Color(Math.Abs(255 - color.R), Math.Abs(255 - color.G), Math.Abs(255 - color.B), color.A);
}
}
/// <summary>
/// A set of utility methods for dealing with <see cref="Color"/> objects
/// </summary>
public static class ColorHelper {
/// <summary>
/// Parses a hexadecimal number into an rgba color.
/// The number should be in the format <c>0xaarrggbb</c>.

View file

@ -1,6 +1,7 @@
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using MLEM.Extensions;
using MLEM.Misc;
namespace MLEM.Font {
@ -37,11 +38,19 @@ namespace MLEM.Font {
/// </summary>
public abstract GenericFont Italic { get; }
///<inheritdoc cref="SpriteFont.LineSpacing"/>
/// <summary>
/// The height of each line of text of this font.
/// This is the value that the text's draw position is offset by every time a newline character is reached.
/// </summary>
public abstract float LineHeight { get; }
///<inheritdoc cref="SpriteFont.MeasureString(string)"/>
protected abstract Vector2 MeasureChar(char c);
/// <summary>
/// Measures the width of the given character with the default scale for use in <see cref="MeasureString"/>.
/// Note that this method does not support <see cref="Nbsp"/>, <see cref="Zwsp"/> and <see cref="OneEmSpace"/> for most generic fonts, which is why <see cref="MeasureString"/> should be used even for single characters.
/// </summary>
/// <param name="c">The character whose width to calculate</param>
/// <returns>The width of the given character with the default scale</returns>
protected abstract float MeasureChar(char c);
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
public abstract void DrawString(SpriteBatch batch, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth);
@ -49,27 +58,34 @@ namespace MLEM.Font {
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
public abstract void DrawString(SpriteBatch batch, StringBuilder text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth);
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
public void DrawString(SpriteBatch batch, string text, Vector2 position, Color color) {
this.DrawString(batch, text, position, color, 0, Vector2.Zero, Vector2.One, SpriteEffects.None, 0);
}
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
public void DrawString(SpriteBatch batch, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects effects, float layerDepth) {
this.DrawString(batch, text, position, color, rotation, origin, new Vector2(scale), effects, layerDepth);
}
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
public void DrawString(SpriteBatch batch, StringBuilder text, Vector2 position, Color color) {
this.DrawString(batch, text, position, color, 0, Vector2.Zero, Vector2.One, SpriteEffects.None, 0);
}
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
public void DrawString(SpriteBatch batch, StringBuilder text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects effects, float layerDepth) {
this.DrawString(batch, text, position, color, rotation, origin, new Vector2(scale), effects, layerDepth);
}
///<inheritdoc cref="SpriteFont.MeasureString(string)"/>
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
public void DrawString(SpriteBatch batch, string text, Vector2 position, Color color) {
this.DrawString(batch, text, position, color, 0, Vector2.Zero, Vector2.One, SpriteEffects.None, 0);
}
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
public void DrawString(SpriteBatch batch, StringBuilder text, Vector2 position, Color color) {
this.DrawString(batch, text, position, color, 0, Vector2.Zero, Vector2.One, SpriteEffects.None, 0);
}
/// <summary>
/// Measures the width of the given string when drawn with this font's underlying font.
/// This method uses <see cref="MeasureChar"/> internally to calculate the size of known characters and calculates additional characters like <see cref="Nbsp"/>, <see cref="Zwsp"/> and <see cref="OneEmSpace"/>.
/// If the text contains newline characters (\n), the size returned will represent a rectangle that encompasses the width of the longest line and the string's full height.
/// </summary>
/// <param name="text">The text whose size to calculate</param>
/// <param name="ignoreTrailingSpaces">Whether trailing whitespace should be ignored in the returned size, causing the end of each line to be effectively trimmed</param>
/// <returns>The size of the string when drawn with this font</returns>
public Vector2 MeasureString(string text, bool ignoreTrailingSpaces = false) {
var size = Vector2.Zero;
if (text.Length <= 0)
@ -85,7 +101,7 @@ namespace MLEM.Font {
xOffset += this.LineHeight;
break;
case Nbsp:
xOffset += this.MeasureChar(' ').X;
xOffset += this.MeasureChar(' ');
break;
case Zwsp:
// don't add width for a zero-width space
@ -96,10 +112,10 @@ namespace MLEM.Font {
i = text.Length - 1;
break;
}
xOffset += this.MeasureChar(' ').X;
xOffset += this.MeasureChar(' ');
break;
default:
xOffset += this.MeasureChar(text[i]).X;
xOffset += this.MeasureChar(text[i]);
break;
}
// increase x size if this line is the longest
@ -164,9 +180,9 @@ namespace MLEM.Font {
widthSinceLastSpace = 0;
currWidth = 0;
} else {
var cWidth = this.MeasureChar(c).X * scale;
var cWidth = this.MeasureString(c.ToCachedString()).X * scale;
if (c == ' ' || c == OneEmSpace || c == Zwsp) {
// remember the location of this space
// remember the location of this (breaking!) space
lastSpaceIndex = ret.Length;
widthSinceLastSpace = 0;
} else if (currWidth + cWidth >= width) {

View file

@ -33,8 +33,8 @@ namespace MLEM.Font {
}
/// <inheritdoc />
protected override Vector2 MeasureChar(char c) {
return this.Font.MeasureString(c.ToCachedString());
protected override float MeasureChar(char c) {
return this.Font.MeasureString(c.ToCachedString()).X;
}
/// <inheritdoc/>

View file

@ -100,7 +100,13 @@ namespace MLEM.Formatting {
/// <param name="scale">The scale that the string is drawn at</param>
/// <returns>The token under the target position</returns>
public Token GetTokenUnderPos(Vector2 stringPos, Vector2 target, float scale) {
return this.Tokens.FirstOrDefault(t => t.GetArea(stringPos, scale).Any(r => r.Contains(target)));
foreach (var token in this.Tokens) {
foreach (var rect in token.GetArea(stringPos, scale)) {
if (rect.Contains(target))
return token;
}
}
return null;
}
/// <inheritdoc cref="GenericFont.DrawString(SpriteBatch,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>

View file

@ -27,19 +27,16 @@ namespace MLEM.Input {
/// <inheritdoc />
public override string ToString() {
var ret = this.Type.ToString();
switch (this.Type) {
case InputType.Mouse:
ret += ((MouseButton) this).ToString();
break;
return $"Mouse{(MouseButton) this}";
case InputType.Keyboard:
ret += ((Keys) this).ToString();
break;
return ((Keys) this).ToString();
case InputType.Gamepad:
ret += ((Buttons) this).ToString();
break;
return $"Gamepad{(Buttons) this}";
default:
return this.Type.ToString();
}
return ret;
}
/// <inheritdoc />

View file

@ -120,13 +120,13 @@ namespace MLEM.Input {
/// An array of all <see cref="Keys"/>, <see cref="Buttons"/> and <see cref="MouseButton"/> values that are currently down.
/// Note that this value only gets set if <see cref="StoreAllActiveInputs"/> is true.
/// </summary>
public GenericInput[] InputsDown { get; private set; }
public GenericInput[] InputsDown { get; private set; } = Array.Empty<GenericInput>();
/// <summary>
/// An array of all <see cref="Keys"/>, <see cref="Buttons"/> and <see cref="MouseButton"/> that are currently considered pressed.
/// An input is considered pressed if it was up in the last update, and is up in the current one.
/// Note that this value only gets set if <see cref="StoreAllActiveInputs"/> is true.
/// </summary>
public GenericInput[] InputsPressed { get; private set; }
public GenericInput[] InputsPressed { get; private set; } = Array.Empty<GenericInput>();
private readonly List<GenericInput> inputsDownAccum = new List<GenericInput>();
/// <summary>
/// Set this field to false to enable <see cref="InputsDown"/> and <see cref="InputsPressed"/> being calculated.
@ -363,7 +363,11 @@ namespace MLEM.Input {
/// <param name="modifier">The modifier key</param>
/// <returns>If the modifier key is down</returns>
public bool IsModifierKeyDown(ModifierKey modifier) {
return modifier.GetKeys().Any(this.IsKeyDown);
foreach (var key in modifier.GetKeys()) {
if (this.IsKeyDown(key))
return true;
}
return false;
}
/// <summary>
@ -570,18 +574,30 @@ namespace MLEM.Input {
}
/// <inheritdoc cref="IsDown"/>
public bool IsAnyDown(params GenericInput[] control) {
return control.Any(c => this.IsDown(c));
public bool IsAnyDown(params GenericInput[] controls) {
foreach (var control in controls) {
if (this.IsDown(control))
return true;
}
return false;
}
/// <inheritdoc cref="IsUp"/>
public bool IsAnyUp(params GenericInput[] control) {
return control.Any(c => this.IsUp(c));
public bool IsAnyUp(params GenericInput[] controls) {
foreach (var control in controls) {
if (this.IsUp(control))
return true;
}
return false;
}
/// <inheritdoc cref="IsPressed"/>
public bool IsAnyPressed(params GenericInput[] control) {
return control.Any(c => this.IsPressed(c));
public bool IsAnyPressed(params GenericInput[] controls) {
foreach (var control in controls) {
if (this.IsPressed(control))
return true;
}
return false;
}
/// <summary>

View file

@ -89,7 +89,11 @@ namespace MLEM.Input {
/// <param name="gamepadIndex">The index of the gamepad to query, or -1 to query all gamepads</param>
/// <returns>Whether this keybind is considered to be down</returns>
public bool IsDown(InputHandler handler, int gamepadIndex = -1) {
return this.combinations.Any(c => c.IsDown(handler, gamepadIndex));
foreach (var combination in this.combinations) {
if (combination.IsDown(handler, gamepadIndex))
return true;
}
return false;
}
/// <summary>
@ -100,7 +104,11 @@ namespace MLEM.Input {
/// <param name="gamepadIndex">The index of the gamepad to query, or -1 to query all gamepads</param>
/// <returns>Whether this keybind is considered to be pressed</returns>
public bool IsPressed(InputHandler handler, int gamepadIndex = -1) {
return this.combinations.Any(c => c.IsPressed(handler, gamepadIndex));
foreach (var combination in this.combinations) {
if (combination.IsPressed(handler, gamepadIndex))
return true;
}
return false;
}
/// <summary>
@ -111,7 +119,11 @@ namespace MLEM.Input {
/// <param name="gamepadIndex">The index of the gamepad to query, or -1 to query all gamepads</param>
/// <returns>Whether any of this keyboard's modifier keys are down</returns>
public bool IsModifierDown(InputHandler handler, int gamepadIndex = -1) {
return this.combinations.Any(c => c.IsModifierDown(handler, gamepadIndex));
foreach (var combination in this.combinations) {
if (combination.IsModifierDown(handler, gamepadIndex))
return true;
}
return false;
}
/// <summary>
@ -123,6 +135,23 @@ namespace MLEM.Input {
yield return combination;
}
/// <summary>
/// Converts this keybind into an easily human-readable string.
/// When using <see cref="ToString()"/>, this method is used with <paramref name="joiner"/> set to ", ".
/// </summary>
/// <param name="joiner">The string to use to join combinations</param>
/// <param name="combinationJoiner">The string to use for combination-internal joining, see <see cref="Combination.ToString(string,System.Func{MLEM.Input.GenericInput,string})"/></param>
/// <param name="inputName">The function to use for determining the display name of generic inputs, see <see cref="Combination.ToString(string,System.Func{MLEM.Input.GenericInput,string})"/></param>
/// <returns>A human-readable string representing this keybind</returns>
public string ToString(string joiner, string combinationJoiner = " + ", Func<GenericInput, string> inputName = null) {
return string.Join(joiner, this.combinations.Select(c => c.ToString(combinationJoiner, inputName)));
}
/// <inheritdoc />
public override string ToString() {
return this.ToString(", ");
}
/// <summary>
/// A key combination is a combination of a set of modifier keys and a key.
/// All of the keys are <see cref="GenericInput"/> instances, so they can be keyboard-, mouse- or gamepad-based.
@ -181,7 +210,29 @@ namespace MLEM.Input {
/// <param name="gamepadIndex">The index of the gamepad to query, or -1 to query all gamepads</param>
/// <returns>Whether this combination's modifiers are down</returns>
public bool IsModifierDown(InputHandler handler, int gamepadIndex = -1) {
return this.Modifiers.Length <= 0 || this.Modifiers.Any(m => handler.IsDown(m, gamepadIndex));
if (this.Modifiers.Length <= 0)
return true;
foreach (var modifier in this.Modifiers) {
if (handler.IsDown(modifier, gamepadIndex))
return true;
}
return false;
}
/// <summary>
/// Converts this combination into an easily human-readable string.
/// When using <see cref="ToString()"/>, this method is used with <paramref name="joiner"/> set to " + ".
/// </summary>
/// <param name="joiner">The string to use to join this combination's <see cref="Modifiers"/> and <see cref="Key"/> together</param>
/// <param name="inputName">The function to use for determining the display name of a <see cref="GenericInput"/>. If this is null, the generic input's default <see cref="GenericInput.ToString"/> method is used.</param>
/// <returns>A human-readable string representing this combination</returns>
public string ToString(string joiner, Func<GenericInput, string> inputName = null) {
return string.Join(joiner, this.Modifiers.Append(this.Key).Select(i => inputName?.Invoke(i) ?? i.ToString()));
}
/// <inheritdoc />
public override string ToString() {
return this.ToString(" + ");
}
}

View file

@ -13,6 +13,14 @@ namespace MLEM.Input {
/// All enum values of <see cref="ModifierKey"/>
/// </summary>
public static readonly ModifierKey[] ModifierKeys = EnumHelper.GetValues<ModifierKey>().ToArray();
private static readonly Dictionary<ModifierKey, Keys[]> KeysLookup = new Dictionary<ModifierKey, Keys[]> {
{ModifierKey.Shift, new[] {Keys.LeftShift, Keys.RightShift}},
{ModifierKey.Control, new[] {Keys.LeftControl, Keys.RightControl}},
{ModifierKey.Alt, new[] {Keys.LeftAlt, Keys.RightAlt}}
};
private static readonly Dictionary<Keys, ModifierKey> ModifiersLookup = KeysLookup
.SelectMany(kv => kv.Value.Select(v => (kv.Key, v)))
.ToDictionary(kv => kv.Item2, kv => kv.Item1);
/// <summary>
/// Returns all of the keys that the given modifier key represents
@ -20,20 +28,7 @@ namespace MLEM.Input {
/// <param name="modifier">The modifier key</param>
/// <returns>All of the keys the modifier key represents</returns>
public static IEnumerable<Keys> GetKeys(this ModifierKey modifier) {
switch (modifier) {
case ModifierKey.Shift:
yield return Keys.LeftShift;
yield return Keys.RightShift;
break;
case ModifierKey.Control:
yield return Keys.LeftControl;
yield return Keys.RightControl;
break;
case ModifierKey.Alt:
yield return Keys.LeftAlt;
yield return Keys.RightAlt;
break;
}
return KeysLookup.TryGetValue(modifier, out var keys) ? keys : Enumerable.Empty<Keys>();
}
/// <summary>
@ -43,11 +38,7 @@ namespace MLEM.Input {
/// <param name="key">The key to convert to a modifier key</param>
/// <returns>The modifier key, or <see cref="ModifierKey.None"/></returns>
public static ModifierKey GetModifier(this Keys key) {
foreach (var mod in ModifierKeys) {
if (GetKeys(mod).Contains(key))
return mod;
}
return ModifierKey.None;
return ModifiersLookup.TryGetValue(key, out var mod) ? mod : ModifierKey.None;
}
/// <inheritdoc cref="GetModifier(Microsoft.Xna.Framework.Input.Keys)"/>

View file

@ -13,6 +13,7 @@
<RepositoryUrl>https://github.com/Ellpeck/MLEM</RepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageIcon>Logo.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>
<ItemGroup>
@ -23,5 +24,6 @@
<ItemGroup>
<None Include="../Media/Logo.png" Pack="true" PackagePath="" />
<None Include="../Docs/index.md" Pack="true" PackagePath="README.md" />
</ItemGroup>
</Project>

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using Microsoft.Xna.Framework;
using static MLEM.Misc.Direction2;
namespace MLEM.Misc {
/// <summary>
@ -83,7 +84,10 @@ namespace MLEM.Misc {
/// <summary>
/// All directions except <see cref="Direction2.None"/>
/// </summary>
public static readonly Direction2[] AllExceptNone = All.Where(dir => dir != Direction2.None).ToArray();
public static readonly Direction2[] AllExceptNone = All.Where(dir => dir != None).ToArray();
private static readonly Direction2[] Clockwise = {Up, UpRight, Right, DownRight, Down, DownLeft, Left, UpLeft};
private static readonly Dictionary<Direction2, int> ClockwiseLookup = Clockwise.Select((d, i) => (d, i)).ToDictionary(kv => kv.d, kv => kv.i);
/// <summary>
/// Returns if the given direction is considered an "adjacent" direction.
@ -92,7 +96,7 @@ namespace MLEM.Misc {
/// <param name="dir">The direction to query</param>
/// <returns>Whether the direction is adjacent</returns>
public static bool IsAdjacent(this Direction2 dir) {
return dir == Direction2.Up || dir == Direction2.Right || dir == Direction2.Down || dir == Direction2.Left;
return dir == Up || dir == Right || dir == Down || dir == Left;
}
/// <summary>
@ -101,7 +105,7 @@ namespace MLEM.Misc {
/// <param name="dir">The direction to query</param>
/// <returns>Whether the direction is diagonal</returns>
public static bool IsDiagonal(this Direction2 dir) {
return dir == Direction2.UpRight || dir == Direction2.DownRight || dir == Direction2.UpLeft || dir == Direction2.DownLeft;
return dir == UpRight || dir == DownRight || dir == UpLeft || dir == DownLeft;
}
/// <summary>
@ -112,21 +116,21 @@ namespace MLEM.Misc {
/// <returns>The direction's offset</returns>
public static Point Offset(this Direction2 dir) {
switch (dir) {
case Direction2.Up:
case Up:
return new Point(0, -1);
case Direction2.Right:
case Right:
return new Point(1, 0);
case Direction2.Down:
case Down:
return new Point(0, 1);
case Direction2.Left:
case Left:
return new Point(-1, 0);
case Direction2.UpRight:
case UpRight:
return new Point(1, -1);
case Direction2.DownRight:
case DownRight:
return new Point(1, 1);
case Direction2.DownLeft:
case DownLeft:
return new Point(-1, 1);
case Direction2.UpLeft:
case UpLeft:
return new Point(-1, -1);
default:
return Point.Zero;
@ -152,24 +156,24 @@ namespace MLEM.Misc {
/// <returns>The opposite of the direction</returns>
public static Direction2 Opposite(this Direction2 dir) {
switch (dir) {
case Direction2.Up:
return Direction2.Down;
case Direction2.Right:
return Direction2.Left;
case Direction2.Down:
return Direction2.Up;
case Direction2.Left:
return Direction2.Right;
case Direction2.UpRight:
return Direction2.DownLeft;
case Direction2.DownRight:
return Direction2.UpLeft;
case Direction2.DownLeft:
return Direction2.UpRight;
case Direction2.UpLeft:
return Direction2.DownRight;
case Up:
return Down;
case Right:
return Left;
case Down:
return Up;
case Left:
return Right;
case UpRight:
return DownLeft;
case DownRight:
return UpLeft;
case DownLeft:
return UpRight;
case UpLeft:
return DownRight;
default:
return Direction2.None;
return None;
}
}
@ -190,26 +194,9 @@ namespace MLEM.Misc {
/// <param name="fortyFiveDegrees">Whether to rotate by 45 degrees. If this is false, the rotation is 90 degrees instead.</param>
/// <returns>The rotated direction</returns>
public static Direction2 RotateCw(this Direction2 dir, bool fortyFiveDegrees = false) {
switch (dir) {
case Direction2.Up:
return fortyFiveDegrees ? Direction2.UpRight : Direction2.Right;
case Direction2.Right:
return fortyFiveDegrees ? Direction2.DownRight : Direction2.Down;
case Direction2.Down:
return fortyFiveDegrees ? Direction2.DownLeft : Direction2.Left;
case Direction2.Left:
return fortyFiveDegrees ? Direction2.UpLeft : Direction2.Up;
case Direction2.UpRight:
return fortyFiveDegrees ? Direction2.Right : Direction2.DownRight;
case Direction2.DownRight:
return fortyFiveDegrees ? Direction2.Down : Direction2.DownLeft;
case Direction2.DownLeft:
return fortyFiveDegrees ? Direction2.Left : Direction2.UpLeft;
case Direction2.UpLeft:
return fortyFiveDegrees ? Direction2.Up : Direction2.UpRight;
default:
return Direction2.None;
}
if (!ClockwiseLookup.TryGetValue(dir, out var dirIndex))
return None;
return Clockwise[(dirIndex + (fortyFiveDegrees ? 1 : 2)) % Clockwise.Length];
}
/// <summary>
@ -219,26 +206,10 @@ namespace MLEM.Misc {
/// <param name="fortyFiveDegrees">Whether to rotate by 45 degrees. If this is false, the rotation is 90 degrees instead.</param>
/// <returns>The rotated direction</returns>
public static Direction2 RotateCcw(this Direction2 dir, bool fortyFiveDegrees = false) {
switch (dir) {
case Direction2.Up:
return fortyFiveDegrees ? Direction2.UpLeft : Direction2.Left;
case Direction2.Right:
return fortyFiveDegrees ? Direction2.UpRight : Direction2.Up;
case Direction2.Down:
return fortyFiveDegrees ? Direction2.DownRight : Direction2.Right;
case Direction2.Left:
return fortyFiveDegrees ? Direction2.DownLeft : Direction2.Down;
case Direction2.UpRight:
return fortyFiveDegrees ? Direction2.Up : Direction2.UpLeft;
case Direction2.DownRight:
return fortyFiveDegrees ? Direction2.Right : Direction2.UpRight;
case Direction2.DownLeft:
return fortyFiveDegrees ? Direction2.Down : Direction2.DownRight;
case Direction2.UpLeft:
return fortyFiveDegrees ? Direction2.Left : Direction2.DownLeft;
default:
return Direction2.None;
}
if (!ClockwiseLookup.TryGetValue(dir, out var dirIndex))
return None;
var index = dirIndex - (fortyFiveDegrees ? 1 : 2);
return Clockwise[index < 0 ? index + Clockwise.Length : index];
}
/// <summary>
@ -252,7 +223,7 @@ namespace MLEM.Misc {
if (Math.Abs(dir.Angle() - offsetAngle) <= MathHelper.PiOver4 / 2)
return dir;
}
return Direction2.None;
return None;
}
/// <summary>
@ -263,10 +234,30 @@ namespace MLEM.Misc {
/// <returns>The vector's direction</returns>
public static Direction2 To90Direction(this Vector2 offset) {
if (offset.X == 0 && offset.Y == 0)
return Direction2.None;
return None;
if (Math.Abs(offset.X) > Math.Abs(offset.Y))
return offset.X > 0 ? Direction2.Right : Direction2.Left;
return offset.Y > 0 ? Direction2.Down : Direction2.Up;
return offset.X > 0 ? Right : Left;
return offset.Y > 0 ? Down : Up;
}
/// <summary>
/// Rotates the given direction by a given reference direction
/// </summary>
/// <param name="dir">The direction to rotate</param>
/// <param name="reference">The direction to rotate by</param>
/// <param name="start">The direction to use as the default direction</param>
/// <returns>The direction, rotated by the reference direction</returns>
public static Direction2 RotateBy(this Direction2 dir, Direction2 reference, Direction2 start = Up) {
if (!ClockwiseLookup.TryGetValue(reference, out var refIndex))
return None;
if (!ClockwiseLookup.TryGetValue(start, out var startIndex))
return None;
if (!ClockwiseLookup.TryGetValue(dir, out var dirIndex))
return None;
var diff = refIndex - startIndex;
if (diff < 0)
diff += Clockwise.Length;
return Clockwise[(dirIndex + diff) % Clockwise.Length];
}
}

View file

@ -1,59 +1,13 @@
using System;
using Microsoft.Xna.Framework.Audio;
using MLEM.Extensions;
namespace MLEM.Misc {
/// <summary>
/// A sound effect info is a wrapper around <see cref="SoundEffect"/> that additionally stores <see cref="Volume"/>, <see cref="Pitch"/> and <see cref="Pan"/> information.
/// Additionally, a <see cref="SoundEffectInstance"/> can be created using <see cref="CreateInstance"/>.
/// </summary>
public class SoundEffectInfo {
/// <inheritdoc />
[Obsolete("This class has been moved to MLEM.Sound.SoundEffectInfo in 5.1.0")]
public class SoundEffectInfo : Sound.SoundEffectInfo {
/// <summary>
/// The <see cref="SoundEffect"/> that is played by this info.
/// </summary>
public readonly SoundEffect Sound;
/// <summary>
/// Volume, ranging from 0.0 (silence) to 1.0 (full volume). Volume during playback is scaled by SoundEffect.MasterVolume.
/// </summary>
public float Volume;
/// <summary>
/// Pitch adjustment, ranging from -1.0 (down an octave) to 0.0 (no change) to 1.0 (up an octave).
/// </summary>
public float Pitch;
/// <summary>
/// Pan value ranging from -1.0 (left speaker) to 0.0 (centered), 1.0 (right speaker).
/// </summary>
public float Pan;
/// <summary>
/// Creates a new sound effect info with the given values.
/// </summary>
/// <param name="sound">The sound to play</param>
/// <param name="volume">The volume to play the sound with</param>
/// <param name="pitch">The pitch to play the sound with</param>
/// <param name="pan">The pan to play the sound with</param>
public SoundEffectInfo(SoundEffect sound, float volume = 1, float pitch = 0, float pan = 0) {
this.Sound = sound;
this.Volume = volume;
this.Pitch = pitch;
this.Pan = pan;
}
/// <summary>
/// Plays this info's <see cref="Sound"/> once.
/// </summary>
/// <returns>False if more sounds are currently playing than the platform allows</returns>
public bool Play() {
return this.Sound.Play(this.Volume, this.Pitch, this.Pan);
}
/// <summary>
/// Creates a new <see cref="SoundEffectInstance"/> with this sound effect info's data.
/// </summary>
/// <param name="isLooped">The value to set the returned instance's <see cref="SoundEffectInstance.IsLooped"/> to. Defaults to false.</param>
/// <returns>A new sound effect instance, with this info's data applied</returns>
public SoundEffectInstance CreateInstance(bool isLooped = false) {
return this.Sound.CreateInstance(this.Volume, this.Pitch, this.Pan, isLooped);
/// <inheritdoc />
public SoundEffectInfo(SoundEffect sound, float volume = 1, float pitch = 0, float pan = 0) : base(sound, volume, pitch, pan) {
}
}

View file

@ -1,141 +1,13 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
namespace MLEM.Misc {
/// <summary>
/// A simple class that handles automatically removing and disposing <see cref="SoundEffectInstance"/> objects once they are done playing to free up the audio source for new sounds.
/// Additionally, a callback can be registered that is invoked when the <see cref="SoundEffectInstance"/> finishes playing.
/// Note that an object of this class can be added to a <see cref="Game"/> using its <see cref="GameComponentCollection"/>.
/// </summary>
public class SoundEffectInstanceHandler : GameComponent, IEnumerable<SoundEffectInstance> {
private readonly List<Entry> playingSounds = new List<Entry>();
private AudioListener[] listeners;
/// <summary>
/// Creates a new sound effect instance handler with the given settings
/// </summary>
/// <param name="game">The game instance</param>
public SoundEffectInstanceHandler(Game game) : base(game) {
}
/// <inheritdoc cref="Update()"/>
public override void Update(GameTime gameTime) {
this.Update();
}
/// <summary>
/// Updates this sound effect handler and manages all of the <see cref="SoundEffectInstance"/> objects in it.
/// If <see cref="SetListeners"/> has been called, all sounds will additionally be updated in 3D space.
/// This should be called each update frame.
/// </summary>
public void Update() {
for (var i = this.playingSounds.Count - 1; i >= 0; i--) {
var entry = this.playingSounds[i];
if (entry.Instance.IsDisposed || entry.Instance.State == SoundState.Stopped) {
entry.Instance.Stop(true);
entry.OnStopped?.Invoke(entry.Instance);
this.playingSounds.RemoveAt(i);
} else {
entry.TryApply3D(this.listeners);
}
}
}
/// <summary>
/// Sets the collection <see cref="AudioListener"/> objects that should be listening to the sounds in this handler in 3D space.
/// If there are one or more listeners, this handler applies 3d effects to all sound effect instances that have been added to this handler along with an <see cref="AudioEmitter"/> in <see cref="Update(Microsoft.Xna.Framework.GameTime)"/> automatically.
/// </summary>
public void SetListeners(params AudioListener[] listeners) {
this.listeners = listeners;
}
/// <summary>
/// Pauses all of the sound effect instances that are currently playing
/// </summary>
public void Pause() {
foreach (var entry in this.playingSounds)
entry.Instance.Pause();
}
/// <summary>
/// Resumes all of the sound effect instances in this handler
/// </summary>
public void Resume() {
foreach (var entry in this.playingSounds)
entry.Instance.Resume();
}
/// <summary>
/// Adds a new <see cref="SoundEffectInstance"/> to this handler.
/// This also starts playing the instance.
/// </summary>
/// <param name="instance">The instance to add</param>
/// <param name="onStopped">The function that should be invoked when this instance stops playing, defaults to null</param>
/// <param name="emitter">An optional audio emitter with which 3d sound can be applied</param>
/// <returns>The passed instance, for chaining</returns>
public SoundEffectInstance Add(SoundEffectInstance instance, Action<SoundEffectInstance> onStopped = null, AudioEmitter emitter = null) {
var entry = new Entry(instance, onStopped, emitter);
this.playingSounds.Add(entry);
instance.Play();
entry.TryApply3D(this.listeners);
return instance;
}
/// <summary>
/// Adds a new <see cref="SoundEffectInfo"/> to this handler.
/// This also starts playing the created instance.
/// </summary>
/// <param name="info">The info for which to add a <see cref="SoundEffectInstance"/></param>
/// <param name="onStopped">The function that should be invoked when this instance stops playing, defaults to null</param>
/// <param name="emitter">An optional audio emitter with which 3d sound can be applied</param>
/// <returns>The newly created <see cref="SoundEffectInstance"/></returns>
public SoundEffectInstance Add(SoundEffectInfo info, Action<SoundEffectInstance> onStopped = null, AudioEmitter emitter = null) {
return this.Add(info.CreateInstance(), onStopped, emitter);
}
/// <summary>
/// Adds a new <see cref="SoundEffect"/> to this handler.
/// This also starts playing the created instance.
/// </summary>
/// <param name="effect">The sound for which to add a <see cref="SoundEffectInstance"/></param>
/// <param name="onStopped">The function that should be invoked when this instance stops playing, defaults to null</param>
/// <param name="emitter">An optional audio emitter with which 3d sound can be applied</param>
/// <returns>The newly created <see cref="SoundEffectInstance"/></returns>
public SoundEffectInstance Add(SoundEffect effect, Action<SoundEffectInstance> onStopped = null, AudioEmitter emitter = null) {
return this.Add(effect.CreateInstance(), onStopped, emitter);
}
/// <inheritdoc />
[Obsolete("This class has been moved to MLEM.Sound.SoundEffectInstanceHandler in 5.1.0")]
public class SoundEffectInstanceHandler : Sound.SoundEffectInstanceHandler {
/// <inheritdoc />
public IEnumerator<SoundEffectInstance> GetEnumerator() {
foreach (var sound in this.playingSounds)
yield return sound.Instance;
}
IEnumerator IEnumerable.GetEnumerator() {
return this.GetEnumerator();
}
private readonly struct Entry {
public readonly SoundEffectInstance Instance;
public readonly Action<SoundEffectInstance> OnStopped;
public readonly AudioEmitter Emitter;
public Entry(SoundEffectInstance instance, Action<SoundEffectInstance> onStopped, AudioEmitter emitter) {
this.Instance = instance;
this.OnStopped = onStopped;
this.Emitter = emitter;
}
public void TryApply3D(AudioListener[] listeners) {
if (listeners != null && listeners.Length > 0 && this.Emitter != null)
this.Instance.Apply3D(listeners, this.Emitter);
}
public SoundEffectInstanceHandler(Game game) : base(game) {
}
}

View file

@ -0,0 +1,60 @@
using Microsoft.Xna.Framework.Audio;
using MLEM.Extensions;
namespace MLEM.Sound {
/// <summary>
/// A sound effect info is a wrapper around <see cref="SoundEffect"/> that additionally stores <see cref="Volume"/>, <see cref="Pitch"/> and <see cref="Pan"/> information.
/// Additionally, a <see cref="SoundEffectInstance"/> can be created using <see cref="CreateInstance"/>.
/// </summary>
public class SoundEffectInfo {
/// <summary>
/// The <see cref="SoundEffect"/> that is played by this info.
/// </summary>
public readonly SoundEffect Sound;
/// <summary>
/// Volume, ranging from 0.0 (silence) to 1.0 (full volume). Volume during playback is scaled by SoundEffect.MasterVolume.
/// </summary>
public float Volume;
/// <summary>
/// Pitch adjustment, ranging from -1.0 (down an octave) to 0.0 (no change) to 1.0 (up an octave).
/// </summary>
public float Pitch;
/// <summary>
/// Pan value ranging from -1.0 (left speaker) to 0.0 (centered), 1.0 (right speaker).
/// </summary>
public float Pan;
/// <summary>
/// Creates a new sound effect info with the given values.
/// </summary>
/// <param name="sound">The sound to play</param>
/// <param name="volume">The volume to play the sound with</param>
/// <param name="pitch">The pitch to play the sound with</param>
/// <param name="pan">The pan to play the sound with</param>
public SoundEffectInfo(SoundEffect sound, float volume = 1, float pitch = 0, float pan = 0) {
this.Sound = sound;
this.Volume = volume;
this.Pitch = pitch;
this.Pan = pan;
}
/// <summary>
/// Plays this info's <see cref="Sound"/> once.
/// </summary>
/// <returns>False if more sounds are currently playing than the platform allows</returns>
public bool Play() {
return this.Sound.Play(this.Volume, this.Pitch, this.Pan);
}
/// <summary>
/// Creates a new <see cref="SoundEffectInstance"/> with this sound effect info's data.
/// </summary>
/// <param name="isLooped">The value to set the returned instance's <see cref="SoundEffectInstance.IsLooped"/> to. Defaults to false.</param>
/// <returns>A new sound effect instance, with this info's data applied</returns>
public SoundEffectInstance CreateInstance(bool isLooped = false) {
return this.Sound.CreateInstance(this.Volume, this.Pitch, this.Pan, isLooped);
}
}
}

View file

@ -0,0 +1,156 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
namespace MLEM.Sound {
/// <summary>
/// A simple class that handles automatically removing and disposing <see cref="SoundEffectInstance"/> objects once they are done playing to free up the audio source for new sounds.
/// Additionally, a callback can be registered that is invoked when the <see cref="SoundEffectInstance"/> finishes playing.
/// Note that an object of this class can be added to a <see cref="Game"/> using its <see cref="GameComponentCollection"/>.
/// </summary>
public class SoundEffectInstanceHandler : GameComponent, IEnumerable<SoundEffectInstanceHandler.Entry> {
private readonly List<Entry> playingSounds = new List<Entry>();
private AudioListener[] listeners;
/// <summary>
/// Creates a new sound effect instance handler with the given settings
/// </summary>
/// <param name="game">The game instance</param>
public SoundEffectInstanceHandler(Game game) : base(game) {
}
/// <inheritdoc cref="Update()"/>
public override void Update(GameTime gameTime) {
this.Update();
}
/// <summary>
/// Updates this sound effect handler and manages all of the <see cref="SoundEffectInstance"/> objects in it.
/// If <see cref="SetListeners"/> has been called, all sounds will additionally be updated in 3D space.
/// This should be called each update frame.
/// </summary>
public void Update() {
for (var i = this.playingSounds.Count - 1; i >= 0; i--) {
var entry = this.playingSounds[i];
if (entry.Instance.IsDisposed || entry.Instance.State == SoundState.Stopped) {
entry.Instance.Stop(true);
entry.OnStopped?.Invoke(entry.Instance);
this.playingSounds.RemoveAt(i);
} else {
entry.TryApply3D(this.listeners);
}
}
}
/// <summary>
/// Sets the collection <see cref="AudioListener"/> objects that should be listening to the sounds in this handler in 3D space.
/// If there are one or more listeners, this handler applies 3d effects to all sound effect instances that have been added to this handler along with an <see cref="AudioEmitter"/> in <see cref="Update(Microsoft.Xna.Framework.GameTime)"/> automatically.
/// </summary>
public void SetListeners(params AudioListener[] listeners) {
this.listeners = listeners;
}
/// <summary>
/// Pauses all of the sound effect instances that are currently playing
/// </summary>
public void Pause() {
foreach (var entry in this.playingSounds)
entry.Instance.Pause();
}
/// <summary>
/// Resumes all of the sound effect instances in this handler
/// </summary>
public void Resume() {
foreach (var entry in this.playingSounds)
entry.Instance.Resume();
}
/// <summary>
/// Adds a new <see cref="SoundEffectInstance"/> to this handler.
/// This also starts playing the instance.
/// </summary>
/// <param name="instance">The instance to add</param>
/// <param name="onStopped">The function that should be invoked when this instance stops playing, defaults to null</param>
/// <param name="emitter">An optional audio emitter with which 3d sound can be applied</param>
/// <returns>The passed instance, for chaining</returns>
public SoundEffectInstance Add(SoundEffectInstance instance, Action<SoundEffectInstance> onStopped = null, AudioEmitter emitter = null) {
var entry = new Entry(instance, onStopped, emitter);
this.playingSounds.Add(entry);
instance.Play();
entry.TryApply3D(this.listeners);
return instance;
}
/// <summary>
/// Adds a new <see cref="Misc.SoundEffectInfo"/> to this handler.
/// This also starts playing the created instance.
/// </summary>
/// <param name="info">The info for which to add a <see cref="SoundEffectInstance"/></param>
/// <param name="onStopped">The function that should be invoked when this instance stops playing, defaults to null</param>
/// <param name="emitter">An optional audio emitter with which 3d sound can be applied</param>
/// <returns>The newly created <see cref="SoundEffectInstance"/></returns>
public SoundEffectInstance Add(SoundEffectInfo info, Action<SoundEffectInstance> onStopped = null, AudioEmitter emitter = null) {
return this.Add(info.CreateInstance(), onStopped, emitter);
}
/// <summary>
/// Adds a new <see cref="SoundEffect"/> to this handler.
/// This also starts playing the created instance.
/// </summary>
/// <param name="effect">The sound for which to add a <see cref="SoundEffectInstance"/></param>
/// <param name="onStopped">The function that should be invoked when this instance stops playing, defaults to null</param>
/// <param name="emitter">An optional audio emitter with which 3d sound can be applied</param>
/// <returns>The newly created <see cref="SoundEffectInstance"/></returns>
public SoundEffectInstance Add(SoundEffect effect, Action<SoundEffectInstance> onStopped = null, AudioEmitter emitter = null) {
return this.Add(effect.CreateInstance(), onStopped, emitter);
}
/// <inheritdoc />
public IEnumerator<Entry> GetEnumerator() {
return this.playingSounds.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator() {
return this.GetEnumerator();
}
/// <summary>
/// An entry in a <see cref="SoundEffectInstanceHandler"/>.
/// Each entry stores the <see cref="SoundEffectInstance"/> that is being played, as well as the additional data passed through <see cref="SoundEffectInstanceHandler.Add(Microsoft.Xna.Framework.Audio.SoundEffectInstance,System.Action{Microsoft.Xna.Framework.Audio.SoundEffectInstance},Microsoft.Xna.Framework.Audio.AudioEmitter)"/>.
/// </summary>
public readonly struct Entry {
/// <summary>
/// The sound effect instance that this entry is playing
/// </summary>
public readonly SoundEffectInstance Instance;
/// <summary>
/// An action that is invoked when this entry's <see cref="Instance"/> is stopped.
/// This action is invoked in <see cref="SoundEffectInstanceHandler.Update()"/>.
/// </summary>
public readonly Action<SoundEffectInstance> OnStopped;
/// <summary>
/// The <see cref="AudioEmitter"/> that this sound effect instance is linked to.
/// If the underlying handler's <see cref="SoundEffectInstanceHandler.SetListeners"/> method has been called, 3D sound will be applied.
/// </summary>
public readonly AudioEmitter Emitter;
internal Entry(SoundEffectInstance instance, Action<SoundEffectInstance> onStopped, AudioEmitter emitter) {
this.Instance = instance;
this.OnStopped = onStopped;
this.Emitter = emitter;
}
internal void TryApply3D(AudioListener[] listeners) {
if (listeners != null && listeners.Length > 0 && this.Emitter != null)
this.Instance.Apply3D(listeners, this.Emitter);
}
}
}
}

View file

@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using MLEM.Extensions;
@ -41,7 +39,9 @@ namespace MLEM.Textures {
this.Region = texture;
this.Padding = padding;
this.Mode = mode;
this.SourceRectangles = this.CreateRectangles(this.Region.Area).ToArray();
this.SourceRectangles = new Rectangle[9];
for (var i = 0; i < this.SourceRectangles.Length; i++)
this.SourceRectangles[i] = (Rectangle) this.GetRectangleForIndex((RectangleF) this.Region.Area, i);
}
/// <summary>
@ -77,7 +77,7 @@ namespace MLEM.Textures {
this(texture, padding, padding, padding, padding, mode) {
}
internal IEnumerable<RectangleF> CreateRectangles(RectangleF area, float patchScale = 1) {
internal RectangleF GetRectangleForIndex(RectangleF area, int index, float patchScale = 1) {
var pl = this.Padding.Left * patchScale;
var pr = this.Padding.Right * patchScale;
var pt = this.Padding.Top * patchScale;
@ -90,19 +90,28 @@ namespace MLEM.Textures {
var topY = area.Y + pt;
var bottomY = area.Y + area.Height - pb;
yield return new RectangleF(area.X, area.Y, pl, pt);
yield return new RectangleF(leftX, area.Y, centerW, pt);
yield return new RectangleF(rightX, area.Y, pr, pt);
yield return new RectangleF(area.X, topY, pl, centerH);
yield return new RectangleF(leftX, topY, centerW, centerH);
yield return new RectangleF(rightX, topY, pr, centerH);
yield return new RectangleF(area.X, bottomY, pl, pb);
yield return new RectangleF(leftX, bottomY, centerW, pb);
yield return new RectangleF(rightX, bottomY, pr, pb);
}
private IEnumerable<Rectangle> CreateRectangles(Rectangle area, float patchScale = 1) {
return this.CreateRectangles((RectangleF) area, patchScale).Select(r => (Rectangle) r);
switch (index) {
case 0:
return new RectangleF(area.X, area.Y, pl, pt);
case 1:
return new RectangleF(leftX, area.Y, centerW, pt);
case 2:
return new RectangleF(rightX, area.Y, pr, pt);
case 3:
return new RectangleF(area.X, topY, pl, centerH);
case 4:
return new RectangleF(leftX, topY, centerW, centerH);
case 5:
return new RectangleF(rightX, topY, pr, centerH);
case 6:
return new RectangleF(area.X, bottomY, pl, pb);
case 7:
return new RectangleF(leftX, bottomY, centerW, pb);
case 8:
return new RectangleF(rightX, bottomY, pr, pb);
default:
throw new ArgumentOutOfRangeException(nameof(index));
}
}
}
@ -143,11 +152,10 @@ namespace MLEM.Textures {
/// <param name="layerDepth">The depth</param>
/// <param name="patchScale">The scale of each area of the nine patch</param>
public static void Draw(this SpriteBatch batch, NinePatch texture, RectangleF destinationRectangle, Color color, float rotation, Vector2 origin, SpriteEffects effects, float layerDepth, float patchScale = 1) {
var destinations = texture.CreateRectangles(destinationRectangle, patchScale);
var count = 0;
foreach (var rect in destinations) {
for (var i = 0; i < texture.SourceRectangles.Length; i++) {
var rect = texture.GetRectangleForIndex(destinationRectangle, i, patchScale);
if (!rect.IsEmpty) {
var src = texture.SourceRectangles[count];
var src = texture.SourceRectangles[i];
switch (texture.Mode) {
case NinePatchMode.Stretch:
batch.Draw(texture.Region.Texture, rect, src, color, rotation, origin, effects, layerDepth);
@ -158,13 +166,12 @@ namespace MLEM.Textures {
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));
batch.Draw(texture.Region.Texture, new RectangleF(rect.Location + new Vector2(x, y), size), new Rectangle(src.Location, (size / patchScale).ToPoint()), color, rotation, origin, effects, layerDepth);
batch.Draw(texture.Region.Texture, new RectangleF(rect.Location + new Vector2(x, y), size), new Rectangle(src.Location, (size / patchScale).CeilCopy().ToPoint()), color, rotation, origin, effects, layerDepth);
}
}
break;
}
}
count++;
}
}

BIN
Media/Banner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -1,11 +1,11 @@
<img src="Media/Logo.png" width="25%" >
![The MLEM logo](https://raw.githubusercontent.com/Ellpeck/MLEM/main/Media/Banner.png)
**MLEM Library for Extending MonoGame** is an addition to the game framework [MonoGame](https://www.monogame.net/) that provides extension methods, quality of life improvements and additional features like a ui system and easy input handling.
# What next?
- Get it on [NuGet](https://www.nuget.org/packages?q=mlem)
- Get prerelease builds on [BaGet](https://nuget.ellpeck.de)
- See the source code in this repository
- See the source code on [GitHub](https://github.com/Ellpeck/MLEM)
- 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
@ -20,16 +20,17 @@ If you created a game with the help of MLEM, you can get it added to this list b
# Gallery
Here are some images that show a couple of MLEM's features.
MLEM.Ui in action:
<img src="Media/Ui.gif">
The [MLEM.Ui](https://mlem.ellpeck.de/articles/ui) demo in action:
MLEM's text formatting system:
<img src="Media/Formatting.png">
![A gif showing various user interface elements from the MLEM.Ui demo](https://raw.githubusercontent.com/Ellpeck/MLEM/main/Media/Ui.gif)
MLEM's [text formatting system](https://mlem.ellpeck.de/articles/text_formatting), which is compatible with both MLEM.Ui and regular sprite batch rendering:
![An image showing text with various colors and other formatting](https://raw.githubusercontent.com/Ellpeck/MLEM/main/Media/Formatting.png)
# Friends of MLEM
There are several other NuGet packages and tools that work well in combination with MonoGame and MLEM. Here are some of them:
- [Contentless](https://github.com/Ellpeck/Contentless), a tool that removes the need to add assets to the MonoGame Content Pipeline manually
- [GameBundle](https://github.com/Ellpeck/GameBundle), a tool that packages MonoGame and other .NET Core applications into several distributable formats
- [ButlerDotNet](https://github.com/Ellpeck/ButlerDotNet), a tool that automatically downloads and invokes itch.io's butler
- [MonoGame.Extended](https://github.com/craftworkgames/MonoGame.Extended), a package that also provides several additional features for MonoGame
- [Coroutine](https://github.com/Ellpeck/Coroutine), a package that implements Unity-style coroutines for any project

View file

@ -11,7 +11,7 @@ with.
<!--
Modify this string to change the font that will be imported.
-->
<FontName>Arial</FontName>
<FontName>Cadman_Roman.otf</FontName>
<!--
Size is a float value, measured in points. Modify this value to change

View file

@ -8,9 +8,11 @@ using Microsoft.Xna.Framework.Input;
using MLEM.Cameras;
using MLEM.Data;
using MLEM.Data.Content;
using MLEM.Extended.Extensions;
using MLEM.Extended.Font;
using MLEM.Extended.Tiled;
using MLEM.Extensions;
using MLEM.Font;
using MLEM.Formatting;
using MLEM.Formatting.Codes;
using MLEM.Input;
@ -20,6 +22,7 @@ using MLEM.Textures;
using MLEM.Ui;
using MLEM.Ui.Elements;
using MLEM.Ui.Style;
using MonoGame.Extended;
using MonoGame.Extended.Tiled;
using Group = MLEM.Ui.Elements.Group;
@ -69,14 +72,15 @@ namespace Sandbox {
textureData[textureData.FromIndex(textureData.ToIndex(25, 9))] = Color.Yellow;
}
var system = new FontSystem(this.GraphicsDevice, 1024, 1024);
var system = new FontSystem();
system.AddFont(File.ReadAllBytes("Content/Fonts/Cadman_Roman.otf"));
//var font = new GenericSpriteFont(LoadContent<SpriteFont>("Fonts/TestFont"));
//var font = new GenericBitmapFont(LoadContent<BitmapFont>("Fonts/Regular"));
var font = new GenericStashFont(system.GetFont(32));
var spriteFont = new GenericSpriteFont(LoadContent<SpriteFont>("Fonts/TestFont"));
this.UiSystem.Style = new UntexturedStyle(this.SpriteBatch) {
Font = font,
TextScale = 0.1F,
TextScale = 0.5F,
PanelTexture = new NinePatch(new TextureRegion(tex, 0, 8, 24, 24), 8),
ButtonTexture = new NinePatch(new TextureRegion(tex, 24, 8, 16, 16), 4)
};
@ -84,6 +88,14 @@ namespace Sandbox {
this.UiSystem.AutoScaleWithScreen = true;
this.UiSystem.GlobalScale = 5;
/*this.OnDraw += (g, time) => {
const string strg = "This is a test string\nto test things\n\nMany things are being tested, like the ability\nfor this font to agree\n\nwith newlines";
this.SpriteBatch.Begin();
spriteFont.DrawString(this.SpriteBatch, strg, new Vector2(600, 100), Color.White, 0, Vector2.Zero, 1, SpriteEffects.None, 0);
font.DrawString(this.SpriteBatch, strg, new Vector2(600, 100), Color.White, 0, Vector2.Zero, 2, SpriteEffects.None, 0);
this.SpriteBatch.End();
};*/
var panel = new Panel(Anchor.Center, new Vector2(0, 100), Vector2.Zero) {SetWidthBasedOnChildren = true};
panel.AddChild(new Button(Anchor.AutoLeft, new Vector2(100, 10)));
panel.AddChild(new Button(Anchor.AutoCenter, new Vector2(80, 10)));
@ -129,7 +141,7 @@ namespace Sandbox {
Console.WriteLine("The res is " + res);
var gradient = this.SpriteBatch.GenerateGradientTexture(Color.Green, Color.Red, Color.Blue, Color.Yellow);
this.OnDraw += (game, time) => {
/*this.OnDraw += (game, time) => {
this.SpriteBatch.Begin();
this.SpriteBatch.Draw(this.SpriteBatch.GetBlankTexture(), new Rectangle(640 - 4, 360 - 4, 8, 8), Color.Green);
@ -141,7 +153,7 @@ namespace Sandbox {
this.SpriteBatch.Draw(gradient, new Rectangle(300, 100, 200, 200), Color.White);
this.SpriteBatch.End();
};
};*/
var sc = 4;
var formatter = new TextFormatter();
@ -218,6 +230,16 @@ namespace Sandbox {
CanBeMoused = false
});
}
var par = loadPanel.AddChild(new Paragraph(Anchor.AutoLeft, 1, "This is another\ntest string\n\nwith many lines\nand many more!"));
par.OnUpdated = (e, time) => {
GenericFont newFont = Input.IsModifierKeyDown(ModifierKey.Shift) ? spriteFont : font;
if (newFont != par.RegularFont.Value) {
par.TextScaleMultiplier = newFont == font ? 1 : 0.5F;
par.RegularFont = newFont;
par.ForceUpdateArea();
}
};
par.OnDrawn = (e, time, batch, a) => batch.DrawRectangle(e.DisplayArea.ToExtended(), Color.Red);
this.UiSystem.Add("Load", loadGroup);
}

View file

@ -18,7 +18,7 @@
<PackageReference Include="MonoGame.Extended.Content.Pipeline" Version="3.8.0" />
<PackageReference Include="MonoGame.Extended.Tiled" Version="3.8.0" />
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.0.1641" />
<PackageReference Include="FontStashSharp.MonoGame" Version="0.9.2" />
<PackageReference Include="FontStashSharp.MonoGame" Version="1.0.3" />
</ItemGroup>
<ItemGroup>

View file

@ -1,14 +1,19 @@
SimpleDeskUp
loc 0 0 48 32
piv 16 16
SimpleDeskRight
loc 48 0 48 32
piv 80 16
Plant
loc 96 0 16 32
LongTableUp
loc 0 32 64 48
piv 16 48
LongTableRight
loc 64 32 64 48
piv 112 48
loc 32 30 64 48
piv 80 46
off 32 2

View file

@ -15,9 +15,15 @@ namespace Tests {
var atlas = DataTextureAtlas.LoadAtlasData(new TextureRegion(texture), game.RawContent, "Texture.atlas");
Assert.AreEqual(atlas.Regions.Count(), 5);
// no added offset
var table = atlas["LongTableUp"];
Assert.AreEqual(table.Area, new Rectangle(0, 32, 64, 48));
Assert.AreEqual(table.PivotPixels, new Vector2(16, 48 - 32));
// added offset
var table2 = atlas["LongTableRight"];
Assert.AreEqual(table2.Area, new Rectangle(64, 32, 64, 48));
Assert.AreEqual(table2.PivotPixels, new Vector2(112 - 64, 48 - 32));
}
}

View file

@ -1,22 +1,64 @@
using Microsoft.Xna.Framework;
using MLEM.Misc;
using NUnit.Framework;
using static MLEM.Misc.Direction2;
namespace Tests {
public class DirectionTests {
[Test]
public void TestDirections() {
Assert.AreEqual(new Vector2(0.5F, 0.5F).ToDirection(), Direction2.DownRight);
Assert.AreEqual(new Vector2(0.25F, 0.5F).ToDirection(), Direction2.DownRight);
Assert.AreEqual(new Vector2(0.15F, 0.5F).ToDirection(), Direction2.Down);
Assert.AreEqual(new Vector2(0.5F, 0.5F).ToDirection(), DownRight);
Assert.AreEqual(new Vector2(0.25F, 0.5F).ToDirection(), DownRight);
Assert.AreEqual(new Vector2(0.15F, 0.5F).ToDirection(), Down);
}
[Test]
public void Test90Directions() {
Assert.AreEqual(new Vector2(0.75F, 0.5F).To90Direction(), Direction2.Right);
Assert.AreEqual(new Vector2(0.5F, 0.5F).To90Direction(), Direction2.Down);
Assert.AreEqual(new Vector2(0.25F, 0.5F).To90Direction(), Direction2.Down);
Assert.AreEqual(new Vector2(0.75F, 0.5F).To90Direction(), Right);
Assert.AreEqual(new Vector2(0.5F, 0.5F).To90Direction(), Down);
Assert.AreEqual(new Vector2(0.25F, 0.5F).To90Direction(), Down);
}
[Test]
public void TestRotations() {
// rotate cw
Assert.AreEqual(Up.RotateCw(), Right);
Assert.AreEqual(Up.RotateCw(true), UpRight);
Assert.AreEqual(Left.RotateCw(), Up);
Assert.AreEqual(UpLeft.RotateCw(), UpRight);
// rotate ccw
Assert.AreEqual(Up.RotateCcw(), Left);
Assert.AreEqual(Up.RotateCcw(true), UpLeft);
Assert.AreEqual(Left.RotateCcw(), Down);
Assert.AreEqual(UpLeft.RotateCcw(), DownLeft);
// rotate 360 degrees
foreach (var dir in Direction2Helper.AllExceptNone) {
Assert.AreEqual(RotateMultipleTimes(dir, true, false, 4), dir);
Assert.AreEqual(RotateMultipleTimes(dir, true, true, 8), dir);
Assert.AreEqual(RotateMultipleTimes(dir, false, false, 4), dir);
Assert.AreEqual(RotateMultipleTimes(dir, false, true, 8), dir);
}
// rotate by with start Up
Assert.AreEqual(Right.RotateBy(Right), Down);
Assert.AreEqual(Right.RotateBy(Down), Left);
Assert.AreEqual(Right.RotateBy(Left), Up);
Assert.AreEqual(Right.RotateBy(Up), Right);
// rotate by with start Left
Assert.AreEqual(Up.RotateBy(Right, Left), Down);
Assert.AreEqual(Up.RotateBy(Down, Left), Left);
Assert.AreEqual(Up.RotateBy(Left, Left), Up);
Assert.AreEqual(Up.RotateBy(Up, Left), Right);
}
private static Direction2 RotateMultipleTimes(Direction2 dir, bool clockwise, bool fortyFiveDegrees, int times) {
for (var i = 0; i < times; i++)
dir = clockwise ? dir.RotateCw(fortyFiveDegrees) : dir.RotateCcw(fortyFiveDegrees);
return dir;
}
}

View file

@ -13,10 +13,13 @@
<ItemGroup>
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.0.1641" />
<PackageReference Include="coverlet.collector" Version="3.0.3" />
<PackageReference Include="coverlet.collector" Version="3.1.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Include="NunitXml.TestLogger" Version="3.0.107" />
</ItemGroup>

View file

@ -2,7 +2,7 @@
#tool docfx.console&version=2.51.0
// this is the upcoming version, for prereleases
var version = Argument("version", "5.0.0");
var version = Argument("version", "5.1.0");
var target = Argument("target", "Default");
var branch = Argument("branch", "main");
var config = Argument("configuration", "Release");