1
0
Fork 0
mirror of https://github.com/Ellpeck/MLEM.git synced 2024-11-24 21:48:35 +01:00

Compare commits

..

No commits in common. "443bb4d6c36f1fec68ac4433842d2fa7292e1fd6" and "19893855dc4affeed5297c621ff83c1b415a6974" have entirely different histories.

48 changed files with 541 additions and 954 deletions

View file

@ -1,60 +1,9 @@
# Changelog # Changelog
MLEM tries to adhere to [semantic versioning](https://semver.org/). MLEM uses [semantic versioning](https://semver.org/).
Jump to version: Jump to version:
- [5.1.0](#510)
- [5.0.0](#500) - [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 ## 5.0.0
### MLEM ### MLEM
Additions Additions

View file

@ -8,7 +8,6 @@ using MLEM.Extensions;
using MLEM.Font; using MLEM.Font;
using MLEM.Formatting; using MLEM.Formatting;
using MLEM.Formatting.Codes; using MLEM.Formatting.Codes;
using MLEM.Input;
using MLEM.Misc; using MLEM.Misc;
using MLEM.Startup; using MLEM.Startup;
using MLEM.Textures; using MLEM.Textures;
@ -49,10 +48,7 @@ namespace Demos {
RadioTexture = new NinePatch(new TextureRegion(this.testTexture, 16, 0, 8, 8), 3), RadioTexture = new NinePatch(new TextureRegion(this.testTexture, 16, 0, 8, 8), 3),
RadioCheckmark = new TextureRegion(this.testTexture, 32, 0, 8, 8) 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 // set the defined style as the current one
this.UiSystem.Style = style; this.UiSystem.Style = style;
// scale every ui up by 5 // scale every ui up by 5
@ -77,7 +73,7 @@ namespace Demos {
}); });
this.root.AddChild(new VerticalSpace(3)); this.root.AddChild(new VerticalSpace(3));
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 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 Button(Anchor.AutoCenter, new Vector2(1, 10), "Change Style") { this.root.AddChild(new Button(Anchor.AutoCenter, new Vector2(1, 10), "Change Style") {
OnPressed = element => this.UiSystem.Style = this.UiSystem.Style == untexturedStyle ? style : untexturedStyle, OnPressed = element => this.UiSystem.Style = this.UiSystem.Style == untexturedStyle ? style : untexturedStyle,
PositionOffset = new Vector2(0, 1), PositionOffset = new Vector2(0, 1),
@ -105,13 +101,6 @@ namespace Demos {
this.root.AddChild(new Paragraph(Anchor.AutoCenter, 1, "Numbers-only input:", true)); 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 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 VerticalSpace(3));
this.root.AddChild(new Paragraph(Anchor.AutoLeft, 1, "Zoom in or out:")); this.root.AddChild(new Paragraph(Anchor.AutoLeft, 1, "Zoom in or out:"));
this.root.AddChild(new Button(Anchor.AutoLeft, new Vector2(10), "+") { this.root.AddChild(new Button(Anchor.AutoLeft, new Vector2(10), "+") {
@ -142,10 +131,6 @@ namespace Demos {
this.root.AddChild(new Button(Anchor.AutoLeft, new Vector2(1, 10), "Toggle Mouse Tooltip") { this.root.AddChild(new Button(Anchor.AutoLeft, new Vector2(1, 10), "Toggle Mouse Tooltip") {
OnPressed = element => tooltip.IsHidden = !tooltip.IsHidden 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) { var slider = new Slider(Anchor.AutoLeft, new Vector2(1, 10), 5, 1) {
StepPerScroll = 0.01F StepPerScroll = 0.01F
@ -193,7 +178,7 @@ namespace Demos {
CoroutineHandler.Start(WobbleProgressBar(bar2)); 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)}); 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)); CoroutineHandler.Start(WobbleProgressBar(bar3));
var bar4 = this.root.AddChild(new ProgressBar(Anchor.AutoInline, new Vector2(8, 30), Direction2.Up, 10) {PositionOffset = new Vector2(1, 0)}); var bar4 = this.root.AddChild(new ProgressBar(Anchor.AutoInline, new Vector2(8, 30), Direction2.Up, 10) {PositionOffset = new Vector2(1, 1)});
CoroutineHandler.Start(WobbleProgressBar(bar4)); CoroutineHandler.Start(WobbleProgressBar(bar4));
this.root.AddChild(new VerticalSpace(3)); this.root.AddChild(new VerticalSpace(3));

View file

@ -1,4 +1,4 @@
![The MLEM logo](https://raw.githubusercontent.com/Ellpeck/MLEM/release/Media/Banner.png) <img src="../Media/Logo.svg" width="25%" >
**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. **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 it on [NuGet](https://www.nuget.org/packages?q=mlem)
- Get prerelease builds on [BaGet](https://nuget.ellpeck.de) - Get prerelease builds on [BaGet](https://nuget.ellpeck.de)
- See the source code on [GitHub](https://github.com/Ellpeck/MLEM) - See the source code on [GitHub](https://github.com/Ellpeck/MLEM)
- See tutorials and API documentation on [the website](https://mlem.ellpeck.de/) - See tutorials and API documentation on this website
- 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) - 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://mlem.ellpeck.de/CHANGELOG.html) for information on updates - See [the changelog](../CHANGELOG.md) for information on updates
# Made with MLEM # 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)) - [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,17 +20,16 @@ If you created a game with the help of MLEM, you can get it added to this list b
# Gallery # Gallery
Here are some images that show a couple of MLEM's features. Here are some images that show a couple of MLEM's features.
The [MLEM.Ui](https://mlem.ellpeck.de/articles/ui) demo in action: MLEM.Ui in action:
<img src="../Media/Ui.gif">
![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:
<img src="../Media/Formatting.png">
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 # 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: 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 - [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 - [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 - [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 - [Coroutine](https://github.com/Ellpeck/Coroutine), a package that implements Unity-style coroutines for any project

View file

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Graphics;
@ -11,7 +12,12 @@ namespace MLEM.Data.Content {
/// </summary> /// </summary>
public class RawContentManager : ContentManager, IGameComponent { public class RawContentManager : ContentManager, IGameComponent {
private static List<RawContentReader> readers; 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 readonly List<IDisposable> disposableAssets = new List<IDisposable>(); private readonly List<IDisposable> disposableAssets = new List<IDisposable>();
@ -51,11 +57,7 @@ namespace MLEM.Data.Content {
private T Read<T>(string assetName, T existing) { private T Read<T>(string assetName, T existing) {
var triedFiles = new List<string>(); var triedFiles = new List<string>();
if (readers == null) foreach (var reader in Readers.Where(r => r.CanRead(typeof(T)))) {
readers = CollectContentReaders();
foreach (var reader in readers) {
if (!reader.CanRead(typeof(T)))
continue;
foreach (var ext in reader.GetFileExtensions()) { foreach (var ext in reader.GetFileExtensions()) {
var file = Path.Combine(this.RootDirectory, $"{assetName}.{ext}"); var file = Path.Combine(this.RootDirectory, $"{assetName}.{ext}");
triedFiles.Add(file); triedFiles.Add(file);
@ -87,30 +89,5 @@ namespace MLEM.Data.Content {
public void Initialize() { 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,5 +1,4 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
@ -10,7 +9,6 @@ namespace MLEM.Data {
public static class CopyExtensions { public static class CopyExtensions {
private const BindingFlags DefaultFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private const BindingFlags DefaultFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
private static readonly Dictionary<Type, ConstructorInfo> ConstructorCache = new Dictionary<Type, ConstructorInfo>();
/// <summary> /// <summary>
/// Creates a shallow copy of the object and returns it. /// Creates a shallow copy of the object and returns it.
@ -87,20 +85,17 @@ namespace MLEM.Data {
} }
private static object Construct(Type t, BindingFlags flags) { private static object Construct(Type t, BindingFlags flags) {
if (!ConstructorCache.TryGetValue(t, out var constructor)) { var constructors = t.GetConstructors(flags);
var constructors = t.GetConstructors(flags); // find a contructor with the correct attribute
// find a contructor with the correct attribute var constructor = constructors.FirstOrDefault(c => c.GetCustomAttribute<CopyConstructorAttribute>() != null);
constructor = constructors.FirstOrDefault(c => c.GetCustomAttribute<CopyConstructorAttribute>() != null); // find a parameterless construcotr
// find a parameterless construcotr if (constructor == null)
if (constructor == null) constructor = t.GetConstructor(flags, null, Type.EmptyTypes, null);
constructor = t.GetConstructor(flags, null, Type.EmptyTypes, null); // fall back to the first constructor
// fall back to the first constructor if (constructor == null)
if (constructor == null) constructor = constructors.FirstOrDefault();
constructor = constructors.FirstOrDefault(); if (constructor == null)
if (constructor == null) throw new NullReferenceException($"Type {t} does not have a constructor with the required visibility");
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]); return constructor.Invoke(new object[constructor.GetParameters().Length]);
} }

View file

@ -9,30 +9,11 @@ using MLEM.Textures;
namespace MLEM.Data { namespace MLEM.Data {
/// <summary> /// <summary>
/// <para>
/// This class represents an atlas of <see cref="TextureRegion"/> objects which are loaded from a special texture atlas file. /// 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 load a data texture atlas, you can use <see cref="DataTextureAtlasExtensions.LoadTextureAtlas"/>.
/// </para> /// 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"/>.
/// <para> /// 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"/>
/// 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> /// </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 { public class DataTextureAtlas {
/// <summary> /// <summary>
@ -75,29 +56,32 @@ namespace MLEM.Data {
text = reader.ReadToEnd(); text = reader.ReadToEnd();
var atlas = new DataTextureAtlas(texture); var atlas = new DataTextureAtlas(texture);
// parse each texture region: "<name> loc <u> <v> <w> <h> [piv <px> <py>] [off <ox> <oy>]" // 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*(?:off\W+([0-9.]+)\W+([0-9.]+))?")) { 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.]+))*")) {
var name = match.Groups[1].Value.Trim(); 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 // location
var loc = new Rectangle( var loc = new Rectangle(
int.Parse(match.Groups[2].Value), int.Parse(match.Groups[3].Value), int.Parse(match.Groups[2].Value), int.Parse(match.Groups[3].Value),
int.Parse(match.Groups[4].Value), int.Parse(match.Groups[5].Value)); int.Parse(match.Groups[4].Value), int.Parse(match.Groups[5].Value));
loc.Offset(off);
// pivot // pivot
var piv = !match.Groups[6].Success ? Vector2.Zero : off + new Vector2( var piv = Vector2.Zero;
float.Parse(match.Groups[6].Value, CultureInfo.InvariantCulture) - (pivotRelative ? 0 : loc.X), if (match.Groups[6].Success) {
float.Parse(match.Groups[7].Value, CultureInfo.InvariantCulture) - (pivotRelative ? 0 : loc.Y)); 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 region = new TextureRegion(texture, loc) { var region = new TextureRegion(texture, loc) {
PivotPixels = piv, PivotPixels = piv,
Name = name 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); atlas.regions.Add(name, region);
} }

View file

@ -12,9 +12,6 @@ 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. /// 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. /// 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}"/>. /// 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> /// </summary>
/// <remarks> /// <remarks>
/// To include enum-like operator overloads in a dynamic enum named MyEnum, the following code can be used: /// To include enum-like operator overloads in a dynamic enum named MyEnum, the following code can be used:
@ -30,7 +27,10 @@ namespace MLEM.Data {
[JsonConverter(typeof(DynamicEnumConverter))] [JsonConverter(typeof(DynamicEnumConverter))]
public abstract class DynamicEnum { public abstract class DynamicEnum {
private static readonly Dictionary<Type, Storage> Storages = new Dictionary<Type, Storage>(); 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 readonly BigInteger value; private readonly BigInteger value;
private Dictionary<DynamicEnum, bool> allFlagsCache; private Dictionary<DynamicEnum, bool> allFlagsCache;
@ -59,6 +59,7 @@ namespace MLEM.Data {
if (this.allFlagsCache == null) if (this.allFlagsCache == null)
this.allFlagsCache = new Dictionary<DynamicEnum, bool>(); this.allFlagsCache = new Dictionary<DynamicEnum, bool>();
if (!this.allFlagsCache.TryGetValue(flags, out var ret)) { 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); ret = (GetValue(this) & GetValue(flags)) == GetValue(flags);
this.allFlagsCache.Add(flags, ret); this.allFlagsCache.Add(flags, ret);
} }
@ -75,6 +76,7 @@ namespace MLEM.Data {
if (this.anyFlagsCache == null) if (this.anyFlagsCache == null)
this.anyFlagsCache = new Dictionary<DynamicEnum, bool>(); this.anyFlagsCache = new Dictionary<DynamicEnum, bool>();
if (!this.anyFlagsCache.TryGetValue(flags, out var ret)) { if (!this.anyFlagsCache.TryGetValue(flags, out var ret)) {
// & is very memory-intensive, so we cache the return value
ret = (GetValue(this) & GetValue(flags)) != 0; ret = (GetValue(this) & GetValue(flags)) != 0;
this.anyFlagsCache.Add(flags, ret); this.anyFlagsCache.Add(flags, ret);
} }
@ -105,20 +107,22 @@ namespace MLEM.Data {
/// <returns>The newly created enum value</returns> /// <returns>The newly created enum value</returns>
/// <exception cref="ArgumentException">Thrown if the name or value passed are already present</exception> /// <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 { public static T Add<T>(string name, BigInteger value) where T : DynamicEnum {
var storage = GetStorage(typeof(T)); if (!Values.TryGetValue(typeof(T), out var dict)) {
dict = new Dictionary<BigInteger, DynamicEnum>();
// cached parsed values and names might be incomplete with new values Values.Add(typeof(T), dict);
storage.ClearCaches();
if (storage.Values.ContainsKey(value))
throw new ArgumentException($"Duplicate value {value}", nameof(value));
foreach (var v in storage.Values.Values) {
if (v.name == name)
throw new ArgumentException($"Duplicate name {name}", nameof(name));
} }
// cached parsed values and names might be incomplete with new values
FlagCache.Remove(typeof(T));
ParseCache.Remove(typeof(T));
if (dict.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));
var ret = Construct(typeof(T), name, value); var ret = Construct(typeof(T), name, value);
storage.Values.Add(value, ret); dict.Add(value, ret);
return (T) ret; return (T) ret;
} }
@ -132,8 +136,10 @@ namespace MLEM.Data {
/// <returns>The newly created enum value</returns> /// <returns>The newly created enum value</returns>
public static T AddValue<T>(string name) where T : DynamicEnum { public static T AddValue<T>(string name) where T : DynamicEnum {
BigInteger value = 0; BigInteger value = 0;
while (GetStorage(typeof(T)).Values.ContainsKey(value)) if (Values.TryGetValue(typeof(T), out var defined)) {
value++; while (defined.ContainsKey(value))
value++;
}
return Add<T>(name, value); return Add<T>(name, value);
} }
@ -146,9 +152,11 @@ namespace MLEM.Data {
/// <typeparam name="T">The type to add this value to</typeparam> /// <typeparam name="T">The type to add this value to</typeparam>
/// <returns>The newly created enum value</returns> /// <returns>The newly created enum value</returns>
public static T AddFlag<T>(string name) where T : DynamicEnum { public static T AddFlag<T>(string name) where T : DynamicEnum {
BigInteger value = 1; BigInteger value = 0;
while (GetStorage(typeof(T)).Values.ContainsKey(value)) if (Values.TryGetValue(typeof(T), out var defined)) {
value <<= 1; while (defined.ContainsKey(value))
value <<= 1;
}
return Add<T>(name, value); return Add<T>(name, value);
} }
@ -169,7 +177,7 @@ namespace MLEM.Data {
/// <param name="type">The type whose values to get</param> /// <param name="type">The type whose values to get</param>
/// <returns>The defined values for the given type</returns> /// <returns>The defined values for the given type</returns>
public static IEnumerable<DynamicEnum> GetValues(Type type) { public static IEnumerable<DynamicEnum> GetValues(Type type) {
return GetStorage(type).Values.Values; return Values.TryGetValue(type, out var ret) ? ret.Values : Enumerable.Empty<DynamicEnum>();
} }
/// <summary> /// <summary>
@ -180,12 +188,7 @@ namespace MLEM.Data {
/// <typeparam name="T">The type of the values</typeparam> /// <typeparam name="T">The type of the values</typeparam>
/// <returns>The bitwise OR (|) combination</returns> /// <returns>The bitwise OR (|) combination</returns>
public static T Or<T>(T left, T right) where T : DynamicEnum { public static T Or<T>(T left, T right) where T : DynamicEnum {
var cache = GetStorage(typeof(T)).OrCache; return GetEnumValue<T>(GetValue(left) | GetValue(right));
if (!cache.TryGetValue((left, right), out var ret)) {
ret = GetEnumValue<T>(GetValue(left) | GetValue(right));
cache.Add((left, right), ret);
}
return (T) ret;
} }
/// <summary> /// <summary>
@ -196,12 +199,7 @@ namespace MLEM.Data {
/// <typeparam name="T">The type of the values</typeparam> /// <typeparam name="T">The type of the values</typeparam>
/// <returns>The bitwise AND (&amp;) combination</returns> /// <returns>The bitwise AND (&amp;) combination</returns>
public static T And<T>(T left, T right) where T : DynamicEnum { public static T And<T>(T left, T right) where T : DynamicEnum {
var cache = GetStorage(typeof(T)).AndCache; return GetEnumValue<T>(GetValue(left) & GetValue(right));
if (!cache.TryGetValue((left, right), out var ret)) {
ret = GetEnumValue<T>(GetValue(left) & GetValue(right));
cache.Add((left, right), ret);
}
return (T) ret;
} }
/// <summary> /// <summary>
@ -212,12 +210,7 @@ namespace MLEM.Data {
/// <typeparam name="T">The type of the values</typeparam> /// <typeparam name="T">The type of the values</typeparam>
/// <returns>The bitwise XOR (^) combination</returns> /// <returns>The bitwise XOR (^) combination</returns>
public static T Xor<T>(T left, T right) where T : DynamicEnum { public static T Xor<T>(T left, T right) where T : DynamicEnum {
var cache = GetStorage(typeof(T)).XorCache; return GetEnumValue<T>(GetValue(left) ^ GetValue(right));
if (!cache.TryGetValue((left, right), out var ret)) {
ret = GetEnumValue<T>(GetValue(left) ^ GetValue(right));
cache.Add((left, right), ret);
}
return (T) ret;
} }
/// <summary> /// <summary>
@ -227,12 +220,7 @@ namespace MLEM.Data {
/// <typeparam name="T">The type of the values</typeparam> /// <typeparam name="T">The type of the values</typeparam>
/// <returns>The bitwise NEG (~) value</returns> /// <returns>The bitwise NEG (~) value</returns>
public static T Neg<T>(T value) where T : DynamicEnum { public static T Neg<T>(T value) where T : DynamicEnum {
var cache = GetStorage(typeof(T)).NegCache; return GetEnumValue<T>(~GetValue(value));
if (!cache.TryGetValue(value, out var ret)) {
ret = GetEnumValue<T>(~GetValue(value));
cache.Add(value, ret);
}
return (T) ret;
} }
/// <summary> /// <summary>
@ -261,16 +249,18 @@ namespace MLEM.Data {
/// <param name="value">The value whose dynamic enum value to get</param> /// <param name="value">The value whose dynamic enum value to get</param>
/// <returns>The defined or combined dynamic enum value</returns> /// <returns>The defined or combined dynamic enum value</returns>
public static DynamicEnum GetEnumValue(Type type, BigInteger value) { public static DynamicEnum GetEnumValue(Type type, BigInteger value) {
var storage = GetStorage(type);
// get the defined value if it exists // get the defined value if it exists
if (storage.Values.TryGetValue(value, out var defined)) if (Values.TryGetValue(type, out var values) && values.TryGetValue(value, out var defined))
return defined; return defined;
// otherwise, cache the combined value // otherwise, cache the combined value
if (!storage.FlagCache.TryGetValue(value, out var combined)) { if (!FlagCache.TryGetValue(type, out var cache)) {
cache = new Dictionary<BigInteger, DynamicEnum>();
FlagCache.Add(type, cache);
}
if (!cache.TryGetValue(value, out var combined)) {
combined = Construct(type, null, value); combined = Construct(type, null, value);
storage.FlagCache.Add(value, combined); cache.Add(value, combined);
} }
return combined; return combined;
} }
@ -295,7 +285,10 @@ namespace MLEM.Data {
/// <param name="strg">The string to parse into a dynamic enum value</param> /// <param name="strg">The string to parse into a dynamic enum value</param>
/// <returns>The parsed enum value, or null if parsing fails</returns> /// <returns>The parsed enum value, or null if parsing fails</returns>
public static DynamicEnum Parse(Type type, string strg) { public static DynamicEnum Parse(Type type, string strg) {
var cache = GetStorage(type).ParseCache; if (!ParseCache.TryGetValue(type, out var cache)) {
cache = new Dictionary<string, DynamicEnum>();
ParseCache.Add(type, cache);
}
if (!cache.TryGetValue(strg, out var cached)) { if (!cache.TryGetValue(strg, out var cached)) {
BigInteger? accum = null; BigInteger? accum = null;
foreach (var val in strg.Split('|')) { foreach (var val in strg.Split('|')) {
@ -313,39 +306,10 @@ namespace MLEM.Data {
return cached; 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) { 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); 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}); 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,7 +13,6 @@
<RepositoryUrl>https://github.com/Ellpeck/MLEM</RepositoryUrl> <RepositoryUrl>https://github.com/Ellpeck/MLEM</RepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageIcon>Logo.png</PackageIcon> <PackageIcon>Logo.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@ -33,6 +32,5 @@
<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> </ItemGroup>
</Project> </Project>

View file

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

View file

@ -18,7 +18,7 @@ namespace MLEM.Extended.Font {
/// <inheritdoc /> /// <inheritdoc />
public override GenericFont Italic { get; } public override GenericFont Italic { get; }
/// <inheritdoc /> /// <inheritdoc />
public override float LineHeight => this.Font.LineHeight; public override float LineHeight { get; }
/// <summary> /// <summary>
/// Creates a new generic font using <see cref="SpriteFontBase"/>. /// Creates a new generic font using <see cref="SpriteFontBase"/>.
@ -29,6 +29,9 @@ namespace MLEM.Extended.Font {
/// <param name="italic">An italic version of the font</param> /// <param name="italic">An italic version of the font</param>
public GenericStashFont(SpriteFontBase font, SpriteFontBase bold = null, SpriteFontBase italic = null) { public GenericStashFont(SpriteFontBase font, SpriteFontBase bold = null, SpriteFontBase italic = null) {
this.Font = font; 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.Bold = bold != null ? new GenericStashFont(bold) : this;
this.Italic = italic != null ? new GenericStashFont(italic) : this; this.Italic = italic != null ? new GenericStashFont(italic) : this;
} }
@ -44,8 +47,8 @@ namespace MLEM.Extended.Font {
} }
/// <inheritdoc /> /// <inheritdoc />
protected override float MeasureChar(char c) { protected override Vector2 MeasureChar(char c) {
return this.Font.MeasureString(c.ToCachedString()).X; return this.Font.MeasureString(c.ToCachedString());
} }
} }

View file

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

View file

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

View file

@ -14,7 +14,6 @@
<RepositoryUrl>https://github.com/Ellpeck/MLEM</RepositoryUrl> <RepositoryUrl>https://github.com/Ellpeck/MLEM</RepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageIcon>Logo.png</PackageIcon> <PackageIcon>Logo.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@ -29,6 +28,5 @@
<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> </ItemGroup>
</Project> </Project>

View file

@ -19,14 +19,12 @@
<PackageProjectUrl>https://mlem.ellpeck.de/</PackageProjectUrl> <PackageProjectUrl>https://mlem.ellpeck.de/</PackageProjectUrl>
<RepositoryUrl>https://github.com/Ellpeck/MLEM</RepositoryUrl> <RepositoryUrl>https://github.com/Ellpeck/MLEM</RepositoryUrl>
<PackageIcon>Logo.png</PackageIcon> <PackageIcon>Logo.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Content Include="content\**\*" Exclude="content\**\.DS_Store;content\**\bin;content\**\obj" /> <Content Include="content\**\*" Exclude="content\**\.DS_Store;content\**\bin;content\**\obj" />
<Compile Remove="**\*" /> <Compile Remove="**\*" />
<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> </ItemGroup>
</Project> </Project>

View file

@ -10,7 +10,6 @@ using MLEM.Input;
using MLEM.Misc; using MLEM.Misc;
using MLEM.Textures; using MLEM.Textures;
using MLEM.Ui.Style; using MLEM.Ui.Style;
using SoundEffectInfo = MLEM.Sound.SoundEffectInfo;
namespace MLEM.Ui.Elements { namespace MLEM.Ui.Elements {
/// <summary> /// <summary>
@ -607,9 +606,7 @@ namespace MLEM.Ui.Elements {
break; break;
case Anchor.AutoInline: case Anchor.AutoInline:
var newX = prevArea.Right + this.ScaledOffset.X; var newX = prevArea.Right + this.ScaledOffset.X;
// with awkward ui scale values, floating point rounding can cause an element that would usually be if (newX + newSize.X <= parentArea.Right) {
// 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.X = newX;
pos.Y = prevArea.Y + this.ScaledOffset.Y; pos.Y = prevArea.Y + this.ScaledOffset.Y;
} else { } else {
@ -651,8 +648,8 @@ namespace MLEM.Ui.Elements {
autoSize.Y = lowest.UnscrolledArea.Bottom - pos.Y + this.ScaledChildPadding.Bottom; autoSize.Y = lowest.UnscrolledArea.Bottom - pos.Y + this.ScaledChildPadding.Bottom;
foundChild = lowest; foundChild = lowest;
} else { } else {
if (this.Children.Any(e => !e.IsHidden)) if (this.Children.Count > 0)
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))})"); 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))})");
autoSize.Y = 0; autoSize.Y = 0;
} }
} }
@ -663,8 +660,8 @@ namespace MLEM.Ui.Elements {
autoSize.X = rightmost.UnscrolledArea.Right - pos.X + this.ScaledChildPadding.Right; autoSize.X = rightmost.UnscrolledArea.Right - pos.X + this.ScaledChildPadding.Right;
foundChild = rightmost; foundChild = rightmost;
} else { } else {
if (this.Children.Any(e => !e.IsHidden)) if (this.Children.Count > 0)
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))})"); 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))})");
autoSize.X = 0; autoSize.X = 0;
} }
} }
@ -675,7 +672,6 @@ namespace MLEM.Ui.Elements {
autoSize = Vector2.Min(autoSize, actualSize); 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)) { if (!autoSize.Equals(this.UnscrolledArea.Size, 0.01F)) {
recursion++; recursion++;
if (recursion >= 16) { if (recursion >= 16) {

View file

@ -131,7 +131,11 @@ 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) { 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() { string GetCurrentName() {
var combination = keybind.GetCombinations().FirstOrDefault(); var combination = keybind.GetCombinations().FirstOrDefault();
return combination?.ToString(" + ", inputName) ?? unboundPlaceholder; if (combination == null)
return unboundPlaceholder;
return string.Join(" + ", combination.Modifiers
.Append(combination.Key)
.Select(i => inputName?.Invoke(i) ?? i.ToString()));
} }
var button = new Button(anchor, size, GetCurrentName()); var button = new Button(anchor, size, GetCurrentName());

View file

@ -151,8 +151,8 @@ namespace MLEM.Ui.Elements {
/// <inheritdoc /> /// <inheritdoc />
protected override void InitStyle(UiStyle style) { protected override void InitStyle(UiStyle style) {
base.InitStyle(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.TextScale.SetFromStyle(style.TextScale);
this.RegularFont.SetFromStyle(style.Font);
this.TextColor.SetFromStyle(style.TextColor); this.TextColor.SetFromStyle(style.TextColor);
} }
@ -236,11 +236,8 @@ namespace MLEM.Ui.Elements {
/// <inheritdoc /> /// <inheritdoc />
public override void ForceUpdateArea() { public override void ForceUpdateArea() {
// set the position offset and size to the token's first area // set the position offset and size to the token's first area
var area = this.Token.GetArea(Vector2.Zero, this.textScale).FirstOrDefault(); var area = this.Token.GetArea(Vector2.Zero, this.textScale).First();
if (this.Parent is Paragraph p) this.PositionOffset = area.Location + new Vector2(((Paragraph) this.Parent).GetAlignmentOffset() / this.Parent.Scale, 0);
area.Location += new Vector2(p.GetAlignmentOffset() / p.Scale, 0);
this.PositionOffset = area.Location;
this.IsHidden = area.IsEmpty;
this.Size = area.Size; this.Size = area.Size;
base.ForceUpdateArea(); base.ForceUpdateArea();
} }

View file

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

View file

@ -1,4 +1,5 @@
using System; using System;
using System.Linq;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using MLEM.Ui.Style; using MLEM.Ui.Style;
@ -24,7 +25,6 @@ namespace MLEM.Ui.Elements {
public Paragraph Paragraph; public Paragraph Paragraph;
private TimeSpan delayCountdown; private TimeSpan delayCountdown;
private bool autoHidden;
/// <summary> /// <summary>
/// Creates a new tooltip with the given settings /// Creates a new tooltip with the given settings
@ -56,14 +56,10 @@ namespace MLEM.Ui.Elements {
base.Update(time); base.Update(time);
this.SnapPositionToMouse(); this.SnapPositionToMouse();
if (this.delayCountdown > TimeSpan.Zero) { if (this.IsHidden && this.delayCountdown > TimeSpan.Zero) {
this.delayCountdown -= time.ElapsedGameTime; this.delayCountdown -= time.ElapsedGameTime;
if (this.delayCountdown <= TimeSpan.Zero) { if (this.delayCountdown <= TimeSpan.Zero)
this.IsHidden = false; this.IsHidden = false;
this.UpdateAutoHidden();
}
} else {
this.UpdateAutoHidden();
} }
} }
@ -72,7 +68,6 @@ namespace MLEM.Ui.Elements {
if (this.Parent != null) if (this.Parent != null)
throw new NotSupportedException($"A tooltip shouldn't be the child of another element ({this.Parent})"); throw new NotSupportedException($"A tooltip shouldn't be the child of another element ({this.Parent})");
base.ForceUpdateArea(); base.ForceUpdateArea();
this.SnapPositionToMouse();
} }
/// <inheritdoc /> /// <inheritdoc />
@ -116,7 +111,6 @@ namespace MLEM.Ui.Elements {
this.IsHidden = true; this.IsHidden = true;
this.delayCountdown = this.Delay; this.delayCountdown = this.Delay;
} }
this.autoHidden = false;
} }
/// <summary> /// <summary>
@ -133,7 +127,11 @@ namespace MLEM.Ui.Elements {
/// </summary> /// </summary>
/// <param name="elementToHover">The element that should automatically cause the tooltip to appear and disappear when hovered and not hovered, respectively</param> /// <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) { public void AddToElement(Element elementToHover) {
elementToHover.OnMouseEnter += element => this.Display(element.System, element.GetType().Name + "Tooltip"); 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.OnMouseExit += element => this.Remove(); elementToHover.OnMouseExit += element => this.Remove();
} }
@ -150,21 +148,5 @@ namespace MLEM.Ui.Elements {
this.AddToElement(elementToHover); 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,11 +13,10 @@
<RepositoryUrl>https://github.com/Ellpeck/MLEM</RepositoryUrl> <RepositoryUrl>https://github.com/Ellpeck/MLEM</RepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageIcon>Logo.png</PackageIcon> <PackageIcon>Logo.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="TextCopy" Version="4.3.1" /> <PackageReference Include="TextCopy" Version="4.3.0" />
<ProjectReference Include="..\MLEM\MLEM.csproj" /> <ProjectReference Include="..\MLEM\MLEM.csproj" />
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.0.1641"> <PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.0.1641">
@ -26,7 +25,6 @@
</ItemGroup> </ItemGroup>
<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> </ItemGroup>
</Project> </Project>

View file

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

View file

@ -1,6 +1,8 @@
using System.Text;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Graphics;
using MLEM.Extensions; using MLEM.Extensions;
using MLEM.Font;
namespace MLEM.Ui.Style { namespace MLEM.Ui.Style {
/// <summary> /// <summary>
@ -35,6 +37,25 @@ namespace MLEM.Ui.Style {
this.ProgressBarColor = Color.White; this.ProgressBarColor = Color.White;
this.ProgressBarProgressPadding = new Vector2(1); this.ProgressBarProgressPadding = new Vector2(1);
this.ProgressBarProgressColor = Color.Red; 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,14 +465,8 @@ namespace MLEM.Ui {
this.CanSelectContent = true; this.CanSelectContent = true;
}; };
this.OnElementRemoved += e => { this.OnElementRemoved += e => {
if (e.CanBeSelected) { if (e.CanBeSelected && !this.Element.GetChildren(regardGrandchildren: true).Any(c => c.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; this.CanSelectContent = false;
}
}; };
} }

View file

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

View file

@ -1,7 +1,6 @@
using System.Text; using System.Text;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Graphics;
using MLEM.Extensions;
using MLEM.Misc; using MLEM.Misc;
namespace MLEM.Font { namespace MLEM.Font {
@ -38,19 +37,11 @@ namespace MLEM.Font {
/// </summary> /// </summary>
public abstract GenericFont Italic { get; } public abstract GenericFont Italic { get; }
/// <summary> ///<inheritdoc cref="SpriteFont.LineSpacing"/>
/// 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; } public abstract float LineHeight { get; }
/// <summary> ///<inheritdoc cref="SpriteFont.MeasureString(string)"/>
/// Measures the width of the given character with the default scale for use in <see cref="MeasureString"/>. protected abstract Vector2 MeasureChar(char c);
/// 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)"/> ///<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); public abstract void DrawString(SpriteBatch batch, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth);
@ -58,34 +49,27 @@ namespace MLEM.Font {
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/> ///<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); 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, 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, 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)"/> ///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
public void DrawString(SpriteBatch batch, string text, Vector2 position, Color color) { 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); 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)"/> ///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
public void DrawString(SpriteBatch batch, StringBuilder text, Vector2 position, Color color) { 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); this.DrawString(batch, text, position, color, 0, Vector2.Zero, Vector2.One, SpriteEffects.None, 0);
} }
/// <summary> ///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
/// Measures the width of the given string when drawn with this font's underlying font. public void DrawString(SpriteBatch batch, StringBuilder text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects effects, float layerDepth) {
/// 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"/>. this.DrawString(batch, text, position, color, rotation, origin, new Vector2(scale), effects, layerDepth);
/// 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> ///<inheritdoc cref="SpriteFont.MeasureString(string)"/>
/// <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) { public Vector2 MeasureString(string text, bool ignoreTrailingSpaces = false) {
var size = Vector2.Zero; var size = Vector2.Zero;
if (text.Length <= 0) if (text.Length <= 0)
@ -101,7 +85,7 @@ namespace MLEM.Font {
xOffset += this.LineHeight; xOffset += this.LineHeight;
break; break;
case Nbsp: case Nbsp:
xOffset += this.MeasureChar(' '); xOffset += this.MeasureChar(' ').X;
break; break;
case Zwsp: case Zwsp:
// don't add width for a zero-width space // don't add width for a zero-width space
@ -112,10 +96,10 @@ namespace MLEM.Font {
i = text.Length - 1; i = text.Length - 1;
break; break;
} }
xOffset += this.MeasureChar(' '); xOffset += this.MeasureChar(' ').X;
break; break;
default: default:
xOffset += this.MeasureChar(text[i]); xOffset += this.MeasureChar(text[i]).X;
break; break;
} }
// increase x size if this line is the longest // increase x size if this line is the longest
@ -180,9 +164,9 @@ namespace MLEM.Font {
widthSinceLastSpace = 0; widthSinceLastSpace = 0;
currWidth = 0; currWidth = 0;
} else { } else {
var cWidth = this.MeasureString(c.ToCachedString()).X * scale; var cWidth = this.MeasureChar(c).X * scale;
if (c == ' ' || c == OneEmSpace || c == Zwsp) { if (c == ' ' || c == OneEmSpace || c == Zwsp) {
// remember the location of this (breaking!) space // remember the location of this space
lastSpaceIndex = ret.Length; lastSpaceIndex = ret.Length;
widthSinceLastSpace = 0; widthSinceLastSpace = 0;
} else if (currWidth + cWidth >= width) { } else if (currWidth + cWidth >= width) {

View file

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

View file

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

View file

@ -27,16 +27,19 @@ namespace MLEM.Input {
/// <inheritdoc /> /// <inheritdoc />
public override string ToString() { public override string ToString() {
var ret = this.Type.ToString();
switch (this.Type) { switch (this.Type) {
case InputType.Mouse: case InputType.Mouse:
return $"Mouse{(MouseButton) this}"; ret += ((MouseButton) this).ToString();
break;
case InputType.Keyboard: case InputType.Keyboard:
return ((Keys) this).ToString(); ret += ((Keys) this).ToString();
break;
case InputType.Gamepad: case InputType.Gamepad:
return $"Gamepad{(Buttons) this}"; ret += ((Buttons) this).ToString();
default: break;
return this.Type.ToString();
} }
return ret;
} }
/// <inheritdoc /> /// <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. /// 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. /// Note that this value only gets set if <see cref="StoreAllActiveInputs"/> is true.
/// </summary> /// </summary>
public GenericInput[] InputsDown { get; private set; } = Array.Empty<GenericInput>(); public GenericInput[] InputsDown { get; private set; }
/// <summary> /// <summary>
/// An array of all <see cref="Keys"/>, <see cref="Buttons"/> and <see cref="MouseButton"/> that are currently considered pressed. /// 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. /// 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. /// Note that this value only gets set if <see cref="StoreAllActiveInputs"/> is true.
/// </summary> /// </summary>
public GenericInput[] InputsPressed { get; private set; } = Array.Empty<GenericInput>(); public GenericInput[] InputsPressed { get; private set; }
private readonly List<GenericInput> inputsDownAccum = new List<GenericInput>(); private readonly List<GenericInput> inputsDownAccum = new List<GenericInput>();
/// <summary> /// <summary>
/// Set this field to false to enable <see cref="InputsDown"/> and <see cref="InputsPressed"/> being calculated. /// Set this field to false to enable <see cref="InputsDown"/> and <see cref="InputsPressed"/> being calculated.
@ -363,11 +363,7 @@ namespace MLEM.Input {
/// <param name="modifier">The modifier key</param> /// <param name="modifier">The modifier key</param>
/// <returns>If the modifier key is down</returns> /// <returns>If the modifier key is down</returns>
public bool IsModifierKeyDown(ModifierKey modifier) { public bool IsModifierKeyDown(ModifierKey modifier) {
foreach (var key in modifier.GetKeys()) { return modifier.GetKeys().Any(this.IsKeyDown);
if (this.IsKeyDown(key))
return true;
}
return false;
} }
/// <summary> /// <summary>
@ -574,30 +570,18 @@ namespace MLEM.Input {
} }
/// <inheritdoc cref="IsDown"/> /// <inheritdoc cref="IsDown"/>
public bool IsAnyDown(params GenericInput[] controls) { public bool IsAnyDown(params GenericInput[] control) {
foreach (var control in controls) { return control.Any(c => this.IsDown(c));
if (this.IsDown(control))
return true;
}
return false;
} }
/// <inheritdoc cref="IsUp"/> /// <inheritdoc cref="IsUp"/>
public bool IsAnyUp(params GenericInput[] controls) { public bool IsAnyUp(params GenericInput[] control) {
foreach (var control in controls) { return control.Any(c => this.IsUp(c));
if (this.IsUp(control))
return true;
}
return false;
} }
/// <inheritdoc cref="IsPressed"/> /// <inheritdoc cref="IsPressed"/>
public bool IsAnyPressed(params GenericInput[] controls) { public bool IsAnyPressed(params GenericInput[] control) {
foreach (var control in controls) { return control.Any(c => this.IsPressed(c));
if (this.IsPressed(control))
return true;
}
return false;
} }
/// <summary> /// <summary>

View file

@ -89,11 +89,7 @@ namespace MLEM.Input {
/// <param name="gamepadIndex">The index of the gamepad to query, or -1 to query all gamepads</param> /// <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> /// <returns>Whether this keybind is considered to be down</returns>
public bool IsDown(InputHandler handler, int gamepadIndex = -1) { public bool IsDown(InputHandler handler, int gamepadIndex = -1) {
foreach (var combination in this.combinations) { return this.combinations.Any(c => c.IsDown(handler, gamepadIndex));
if (combination.IsDown(handler, gamepadIndex))
return true;
}
return false;
} }
/// <summary> /// <summary>
@ -104,11 +100,7 @@ namespace MLEM.Input {
/// <param name="gamepadIndex">The index of the gamepad to query, or -1 to query all gamepads</param> /// <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> /// <returns>Whether this keybind is considered to be pressed</returns>
public bool IsPressed(InputHandler handler, int gamepadIndex = -1) { public bool IsPressed(InputHandler handler, int gamepadIndex = -1) {
foreach (var combination in this.combinations) { return this.combinations.Any(c => c.IsPressed(handler, gamepadIndex));
if (combination.IsPressed(handler, gamepadIndex))
return true;
}
return false;
} }
/// <summary> /// <summary>
@ -119,11 +111,7 @@ namespace MLEM.Input {
/// <param name="gamepadIndex">The index of the gamepad to query, or -1 to query all gamepads</param> /// <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> /// <returns>Whether any of this keyboard's modifier keys are down</returns>
public bool IsModifierDown(InputHandler handler, int gamepadIndex = -1) { public bool IsModifierDown(InputHandler handler, int gamepadIndex = -1) {
foreach (var combination in this.combinations) { return this.combinations.Any(c => c.IsModifierDown(handler, gamepadIndex));
if (combination.IsModifierDown(handler, gamepadIndex))
return true;
}
return false;
} }
/// <summary> /// <summary>
@ -135,23 +123,6 @@ namespace MLEM.Input {
yield return combination; 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> /// <summary>
/// A key combination is a combination of a set of modifier keys and a key. /// 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. /// All of the keys are <see cref="GenericInput"/> instances, so they can be keyboard-, mouse- or gamepad-based.
@ -210,29 +181,7 @@ namespace MLEM.Input {
/// <param name="gamepadIndex">The index of the gamepad to query, or -1 to query all gamepads</param> /// <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> /// <returns>Whether this combination's modifiers are down</returns>
public bool IsModifierDown(InputHandler handler, int gamepadIndex = -1) { public bool IsModifierDown(InputHandler handler, int gamepadIndex = -1) {
if (this.Modifiers.Length <= 0) return this.Modifiers.Length <= 0 || this.Modifiers.Any(m => handler.IsDown(m, gamepadIndex));
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,14 +13,6 @@ namespace MLEM.Input {
/// All enum values of <see cref="ModifierKey"/> /// All enum values of <see cref="ModifierKey"/>
/// </summary> /// </summary>
public static readonly ModifierKey[] ModifierKeys = EnumHelper.GetValues<ModifierKey>().ToArray(); 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> /// <summary>
/// Returns all of the keys that the given modifier key represents /// Returns all of the keys that the given modifier key represents
@ -28,7 +20,20 @@ namespace MLEM.Input {
/// <param name="modifier">The modifier key</param> /// <param name="modifier">The modifier key</param>
/// <returns>All of the keys the modifier key represents</returns> /// <returns>All of the keys the modifier key represents</returns>
public static IEnumerable<Keys> GetKeys(this ModifierKey modifier) { public static IEnumerable<Keys> GetKeys(this ModifierKey modifier) {
return KeysLookup.TryGetValue(modifier, out var keys) ? keys : Enumerable.Empty<Keys>(); 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;
}
} }
/// <summary> /// <summary>
@ -38,7 +43,11 @@ namespace MLEM.Input {
/// <param name="key">The key to convert to a modifier key</param> /// <param name="key">The key to convert to a modifier key</param>
/// <returns>The modifier key, or <see cref="ModifierKey.None"/></returns> /// <returns>The modifier key, or <see cref="ModifierKey.None"/></returns>
public static ModifierKey GetModifier(this Keys key) { public static ModifierKey GetModifier(this Keys key) {
return ModifiersLookup.TryGetValue(key, out var mod) ? mod : ModifierKey.None; foreach (var mod in ModifierKeys) {
if (GetKeys(mod).Contains(key))
return mod;
}
return ModifierKey.None;
} }
/// <inheritdoc cref="GetModifier(Microsoft.Xna.Framework.Input.Keys)"/> /// <inheritdoc cref="GetModifier(Microsoft.Xna.Framework.Input.Keys)"/>

View file

@ -13,7 +13,6 @@
<RepositoryUrl>https://github.com/Ellpeck/MLEM</RepositoryUrl> <RepositoryUrl>https://github.com/Ellpeck/MLEM</RepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageIcon>Logo.png</PackageIcon> <PackageIcon>Logo.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@ -24,6 +23,5 @@
<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> </ItemGroup>
</Project> </Project>

View file

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.Serialization; using System.Runtime.Serialization;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using static MLEM.Misc.Direction2;
namespace MLEM.Misc { namespace MLEM.Misc {
/// <summary> /// <summary>
@ -84,10 +83,7 @@ namespace MLEM.Misc {
/// <summary> /// <summary>
/// All directions except <see cref="Direction2.None"/> /// All directions except <see cref="Direction2.None"/>
/// </summary> /// </summary>
public static readonly Direction2[] AllExceptNone = All.Where(dir => dir != None).ToArray(); public static readonly Direction2[] AllExceptNone = All.Where(dir => dir != Direction2.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> /// <summary>
/// Returns if the given direction is considered an "adjacent" direction. /// Returns if the given direction is considered an "adjacent" direction.
@ -96,7 +92,7 @@ namespace MLEM.Misc {
/// <param name="dir">The direction to query</param> /// <param name="dir">The direction to query</param>
/// <returns>Whether the direction is adjacent</returns> /// <returns>Whether the direction is adjacent</returns>
public static bool IsAdjacent(this Direction2 dir) { public static bool IsAdjacent(this Direction2 dir) {
return dir == Up || dir == Right || dir == Down || dir == Left; return dir == Direction2.Up || dir == Direction2.Right || dir == Direction2.Down || dir == Direction2.Left;
} }
/// <summary> /// <summary>
@ -105,7 +101,7 @@ namespace MLEM.Misc {
/// <param name="dir">The direction to query</param> /// <param name="dir">The direction to query</param>
/// <returns>Whether the direction is diagonal</returns> /// <returns>Whether the direction is diagonal</returns>
public static bool IsDiagonal(this Direction2 dir) { public static bool IsDiagonal(this Direction2 dir) {
return dir == UpRight || dir == DownRight || dir == UpLeft || dir == DownLeft; return dir == Direction2.UpRight || dir == Direction2.DownRight || dir == Direction2.UpLeft || dir == Direction2.DownLeft;
} }
/// <summary> /// <summary>
@ -116,21 +112,21 @@ namespace MLEM.Misc {
/// <returns>The direction's offset</returns> /// <returns>The direction's offset</returns>
public static Point Offset(this Direction2 dir) { public static Point Offset(this Direction2 dir) {
switch (dir) { switch (dir) {
case Up: case Direction2.Up:
return new Point(0, -1); return new Point(0, -1);
case Right: case Direction2.Right:
return new Point(1, 0); return new Point(1, 0);
case Down: case Direction2.Down:
return new Point(0, 1); return new Point(0, 1);
case Left: case Direction2.Left:
return new Point(-1, 0); return new Point(-1, 0);
case UpRight: case Direction2.UpRight:
return new Point(1, -1); return new Point(1, -1);
case DownRight: case Direction2.DownRight:
return new Point(1, 1); return new Point(1, 1);
case DownLeft: case Direction2.DownLeft:
return new Point(-1, 1); return new Point(-1, 1);
case UpLeft: case Direction2.UpLeft:
return new Point(-1, -1); return new Point(-1, -1);
default: default:
return Point.Zero; return Point.Zero;
@ -156,24 +152,24 @@ namespace MLEM.Misc {
/// <returns>The opposite of the direction</returns> /// <returns>The opposite of the direction</returns>
public static Direction2 Opposite(this Direction2 dir) { public static Direction2 Opposite(this Direction2 dir) {
switch (dir) { switch (dir) {
case Up: case Direction2.Up:
return Down; return Direction2.Down;
case Right: case Direction2.Right:
return Left; return Direction2.Left;
case Down: case Direction2.Down:
return Up; return Direction2.Up;
case Left: case Direction2.Left:
return Right; return Direction2.Right;
case UpRight: case Direction2.UpRight:
return DownLeft; return Direction2.DownLeft;
case DownRight: case Direction2.DownRight:
return UpLeft; return Direction2.UpLeft;
case DownLeft: case Direction2.DownLeft:
return UpRight; return Direction2.UpRight;
case UpLeft: case Direction2.UpLeft:
return DownRight; return Direction2.DownRight;
default: default:
return None; return Direction2.None;
} }
} }
@ -194,9 +190,26 @@ namespace MLEM.Misc {
/// <param name="fortyFiveDegrees">Whether to rotate by 45 degrees. If this is false, the rotation is 90 degrees instead.</param> /// <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> /// <returns>The rotated direction</returns>
public static Direction2 RotateCw(this Direction2 dir, bool fortyFiveDegrees = false) { public static Direction2 RotateCw(this Direction2 dir, bool fortyFiveDegrees = false) {
if (!ClockwiseLookup.TryGetValue(dir, out var dirIndex)) switch (dir) {
return None; case Direction2.Up:
return Clockwise[(dirIndex + (fortyFiveDegrees ? 1 : 2)) % Clockwise.Length]; 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;
}
} }
/// <summary> /// <summary>
@ -206,10 +219,26 @@ namespace MLEM.Misc {
/// <param name="fortyFiveDegrees">Whether to rotate by 45 degrees. If this is false, the rotation is 90 degrees instead.</param> /// <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> /// <returns>The rotated direction</returns>
public static Direction2 RotateCcw(this Direction2 dir, bool fortyFiveDegrees = false) { public static Direction2 RotateCcw(this Direction2 dir, bool fortyFiveDegrees = false) {
if (!ClockwiseLookup.TryGetValue(dir, out var dirIndex)) switch (dir) {
return None; case Direction2.Up:
var index = dirIndex - (fortyFiveDegrees ? 1 : 2); return fortyFiveDegrees ? Direction2.UpLeft : Direction2.Left;
return Clockwise[index < 0 ? index + Clockwise.Length : index]; 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;
}
} }
/// <summary> /// <summary>
@ -223,7 +252,7 @@ namespace MLEM.Misc {
if (Math.Abs(dir.Angle() - offsetAngle) <= MathHelper.PiOver4 / 2) if (Math.Abs(dir.Angle() - offsetAngle) <= MathHelper.PiOver4 / 2)
return dir; return dir;
} }
return None; return Direction2.None;
} }
/// <summary> /// <summary>
@ -234,30 +263,10 @@ namespace MLEM.Misc {
/// <returns>The vector's direction</returns> /// <returns>The vector's direction</returns>
public static Direction2 To90Direction(this Vector2 offset) { public static Direction2 To90Direction(this Vector2 offset) {
if (offset.X == 0 && offset.Y == 0) if (offset.X == 0 && offset.Y == 0)
return None; return Direction2.None;
if (Math.Abs(offset.X) > Math.Abs(offset.Y)) if (Math.Abs(offset.X) > Math.Abs(offset.Y))
return offset.X > 0 ? Right : Left; return offset.X > 0 ? Direction2.Right : Direction2.Left;
return offset.Y > 0 ? Down : Up; return offset.Y > 0 ? Direction2.Down : Direction2.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,13 +1,59 @@
using System;
using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Audio;
using MLEM.Extensions;
namespace MLEM.Misc { namespace MLEM.Misc {
/// <inheritdoc /> /// <summary>
[Obsolete("This class has been moved to MLEM.Sound.SoundEffectInfo in 5.1.0")] /// 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.
public class SoundEffectInfo : Sound.SoundEffectInfo { /// Additionally, a <see cref="SoundEffectInstance"/> can be created using <see cref="CreateInstance"/>.
/// </summary>
public class SoundEffectInfo {
/// <inheritdoc /> /// <summary>
public SoundEffectInfo(SoundEffect sound, float volume = 1, float pitch = 0, float pan = 0) : base(sound, volume, pitch, pan) { /// 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

@ -1,13 +1,141 @@
using System; using System;
using System.Collections;
using System.Collections.Generic;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
namespace MLEM.Misc { namespace MLEM.Misc {
/// <inheritdoc /> /// <summary>
[Obsolete("This class has been moved to MLEM.Sound.SoundEffectInstanceHandler in 5.1.0")] /// 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.
public class SoundEffectInstanceHandler : Sound.SoundEffectInstanceHandler { /// 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 /> /// <inheritdoc />
public SoundEffectInstanceHandler(Game game) : base(game) { 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);
}
} }
} }

View file

@ -1,60 +0,0 @@
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

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View file

@ -1,11 +1,11 @@
![The MLEM logo](https://raw.githubusercontent.com/Ellpeck/MLEM/main/Media/Banner.png) <img src="Media/Logo.png" width="25%" >
**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. **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? # What next?
- Get it on [NuGet](https://www.nuget.org/packages?q=mlem) - Get it on [NuGet](https://www.nuget.org/packages?q=mlem)
- Get prerelease builds on [BaGet](https://nuget.ellpeck.de) - Get prerelease builds on [BaGet](https://nuget.ellpeck.de)
- See the source code on [GitHub](https://github.com/Ellpeck/MLEM) - See the source code in this repository
- See tutorials and API documentation on [the website](https://mlem.ellpeck.de/) - 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) - 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 - See [the changelog](https://github.com/Ellpeck/MLEM/blob/main/CHANGELOG.md) for information on updates
@ -20,17 +20,16 @@ If you created a game with the help of MLEM, you can get it added to this list b
# Gallery # Gallery
Here are some images that show a couple of MLEM's features. Here are some images that show a couple of MLEM's features.
The [MLEM.Ui](https://mlem.ellpeck.de/articles/ui) demo in action: MLEM.Ui in action:
<img src="Media/Ui.gif">
![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:
<img src="Media/Formatting.png">
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 # 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: 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 - [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 - [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 - [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 - [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. Modify this string to change the font that will be imported.
--> -->
<FontName>Cadman_Roman.otf</FontName> <FontName>Arial</FontName>
<!-- <!--
Size is a float value, measured in points. Modify this value to change Size is a float value, measured in points. Modify this value to change

View file

@ -8,11 +8,9 @@ using Microsoft.Xna.Framework.Input;
using MLEM.Cameras; using MLEM.Cameras;
using MLEM.Data; using MLEM.Data;
using MLEM.Data.Content; using MLEM.Data.Content;
using MLEM.Extended.Extensions;
using MLEM.Extended.Font; using MLEM.Extended.Font;
using MLEM.Extended.Tiled; using MLEM.Extended.Tiled;
using MLEM.Extensions; using MLEM.Extensions;
using MLEM.Font;
using MLEM.Formatting; using MLEM.Formatting;
using MLEM.Formatting.Codes; using MLEM.Formatting.Codes;
using MLEM.Input; using MLEM.Input;
@ -22,7 +20,6 @@ using MLEM.Textures;
using MLEM.Ui; using MLEM.Ui;
using MLEM.Ui.Elements; using MLEM.Ui.Elements;
using MLEM.Ui.Style; using MLEM.Ui.Style;
using MonoGame.Extended;
using MonoGame.Extended.Tiled; using MonoGame.Extended.Tiled;
using Group = MLEM.Ui.Elements.Group; using Group = MLEM.Ui.Elements.Group;
@ -72,15 +69,14 @@ namespace Sandbox {
textureData[textureData.FromIndex(textureData.ToIndex(25, 9))] = Color.Yellow; textureData[textureData.FromIndex(textureData.ToIndex(25, 9))] = Color.Yellow;
} }
var system = new FontSystem(); var system = new FontSystem(this.GraphicsDevice, 1024, 1024);
system.AddFont(File.ReadAllBytes("Content/Fonts/Cadman_Roman.otf")); system.AddFont(File.ReadAllBytes("Content/Fonts/Cadman_Roman.otf"));
//var font = new GenericSpriteFont(LoadContent<SpriteFont>("Fonts/TestFont")); //var font = new GenericSpriteFont(LoadContent<SpriteFont>("Fonts/TestFont"));
//var font = new GenericBitmapFont(LoadContent<BitmapFont>("Fonts/Regular")); //var font = new GenericBitmapFont(LoadContent<BitmapFont>("Fonts/Regular"));
var font = new GenericStashFont(system.GetFont(32)); var font = new GenericStashFont(system.GetFont(32));
var spriteFont = new GenericSpriteFont(LoadContent<SpriteFont>("Fonts/TestFont"));
this.UiSystem.Style = new UntexturedStyle(this.SpriteBatch) { this.UiSystem.Style = new UntexturedStyle(this.SpriteBatch) {
Font = font, Font = font,
TextScale = 0.5F, TextScale = 0.1F,
PanelTexture = new NinePatch(new TextureRegion(tex, 0, 8, 24, 24), 8), PanelTexture = new NinePatch(new TextureRegion(tex, 0, 8, 24, 24), 8),
ButtonTexture = new NinePatch(new TextureRegion(tex, 24, 8, 16, 16), 4) ButtonTexture = new NinePatch(new TextureRegion(tex, 24, 8, 16, 16), 4)
}; };
@ -88,14 +84,6 @@ namespace Sandbox {
this.UiSystem.AutoScaleWithScreen = true; this.UiSystem.AutoScaleWithScreen = true;
this.UiSystem.GlobalScale = 5; 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}; 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.AutoLeft, new Vector2(100, 10)));
panel.AddChild(new Button(Anchor.AutoCenter, new Vector2(80, 10))); panel.AddChild(new Button(Anchor.AutoCenter, new Vector2(80, 10)));
@ -141,7 +129,7 @@ namespace Sandbox {
Console.WriteLine("The res is " + res); Console.WriteLine("The res is " + res);
var gradient = this.SpriteBatch.GenerateGradientTexture(Color.Green, Color.Red, Color.Blue, Color.Yellow); 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.Begin();
this.SpriteBatch.Draw(this.SpriteBatch.GetBlankTexture(), new Rectangle(640 - 4, 360 - 4, 8, 8), Color.Green); this.SpriteBatch.Draw(this.SpriteBatch.GetBlankTexture(), new Rectangle(640 - 4, 360 - 4, 8, 8), Color.Green);
@ -153,7 +141,7 @@ namespace Sandbox {
this.SpriteBatch.Draw(gradient, new Rectangle(300, 100, 200, 200), Color.White); this.SpriteBatch.Draw(gradient, new Rectangle(300, 100, 200, 200), Color.White);
this.SpriteBatch.End(); this.SpriteBatch.End();
};*/ };
var sc = 4; var sc = 4;
var formatter = new TextFormatter(); var formatter = new TextFormatter();
@ -230,16 +218,6 @@ namespace Sandbox {
CanBeMoused = false 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); 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.Content.Pipeline" Version="3.8.0" />
<PackageReference Include="MonoGame.Extended.Tiled" 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="MonoGame.Framework.DesktopGL" Version="3.8.0.1641" />
<PackageReference Include="FontStashSharp.MonoGame" Version="1.0.3" /> <PackageReference Include="FontStashSharp.MonoGame" Version="0.9.2" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

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

View file

@ -15,15 +15,9 @@ namespace Tests {
var atlas = DataTextureAtlas.LoadAtlasData(new TextureRegion(texture), game.RawContent, "Texture.atlas"); var atlas = DataTextureAtlas.LoadAtlasData(new TextureRegion(texture), game.RawContent, "Texture.atlas");
Assert.AreEqual(atlas.Regions.Count(), 5); Assert.AreEqual(atlas.Regions.Count(), 5);
// no added offset
var table = atlas["LongTableUp"]; var table = atlas["LongTableUp"];
Assert.AreEqual(table.Area, new Rectangle(0, 32, 64, 48)); Assert.AreEqual(table.Area, new Rectangle(0, 32, 64, 48));
Assert.AreEqual(table.PivotPixels, new Vector2(16, 48 - 32)); 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,64 +1,22 @@
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using MLEM.Misc; using MLEM.Misc;
using NUnit.Framework; using NUnit.Framework;
using static MLEM.Misc.Direction2;
namespace Tests { namespace Tests {
public class DirectionTests { public class DirectionTests {
[Test] [Test]
public void TestDirections() { public void TestDirections() {
Assert.AreEqual(new Vector2(0.5F, 0.5F).ToDirection(), DownRight); Assert.AreEqual(new Vector2(0.5F, 0.5F).ToDirection(), Direction2.DownRight);
Assert.AreEqual(new Vector2(0.25F, 0.5F).ToDirection(), DownRight); Assert.AreEqual(new Vector2(0.25F, 0.5F).ToDirection(), Direction2.DownRight);
Assert.AreEqual(new Vector2(0.15F, 0.5F).ToDirection(), Down); Assert.AreEqual(new Vector2(0.15F, 0.5F).ToDirection(), Direction2.Down);
} }
[Test] [Test]
public void Test90Directions() { public void Test90Directions() {
Assert.AreEqual(new Vector2(0.75F, 0.5F).To90Direction(), Right); Assert.AreEqual(new Vector2(0.75F, 0.5F).To90Direction(), Direction2.Right);
Assert.AreEqual(new Vector2(0.5F, 0.5F).To90Direction(), Down); Assert.AreEqual(new Vector2(0.5F, 0.5F).To90Direction(), Direction2.Down);
Assert.AreEqual(new Vector2(0.25F, 0.5F).To90Direction(), Down); Assert.AreEqual(new Vector2(0.25F, 0.5F).To90Direction(), Direction2.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,13 +13,10 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.0.1641" /> <PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.0.1641" />
<PackageReference Include="coverlet.collector" Version="3.1.0"> <PackageReference Include="coverlet.collector" Version="3.0.3" />
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" /> <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Include="NunitXml.TestLogger" Version="3.0.107" /> <PackageReference Include="NunitXml.TestLogger" Version="3.0.107" />
</ItemGroup> </ItemGroup>

View file

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