mirror of
https://github.com/Ellpeck/MLEM.git
synced 2024-11-24 13:38:34 +01:00
Compare commits
47 commits
19893855dc
...
443bb4d6c3
Author | SHA1 | Date | |
---|---|---|---|
443bb4d6c3 | |||
41b924ef34 | |||
a140e85300 | |||
a53939837f | |||
e620ed0d87 | |||
81dcbfb9a1 | |||
1bbb12a1fa | |||
9890c4895c | |||
05e320d4f4 | |||
54e3c98029 | |||
6537ff00c1 | |||
866dad49ab | |||
ff510c54c5 | |||
51833d523d | |||
a9a7f2b421 | |||
094de058c4 | |||
db7ee04d30 | |||
0c45f2d8e6 | |||
e11eb459b8 | |||
516265bf5b | |||
57f8e56c38 | |||
8fac4a0b69 | |||
01bec459de | |||
9eef1e5b1c | |||
bb9b322580 | |||
e53d30e5ca | |||
ebc6ec872b | |||
527c4af3e4 | |||
1067055bb5 | |||
a76c14b243 | |||
abac738123 | |||
374d936be2 | |||
a52b46dce9 | |||
6aa9ec03d4 | |||
9a0b8ef846 | |||
27fc5a74d9 | |||
ee2b0b82fe | |||
bb189261d7 | |||
8d92131630 | |||
f352e6b437 | |||
d1b229b589 | |||
642608a8a2 | |||
f71eb6eddb | |||
00d9ee99d8 | |||
579fd38533 | |||
58bd076e2a | |||
25efa0bd50 |
48 changed files with 950 additions and 537 deletions
53
CHANGELOG.md
53
CHANGELOG.md
|
@ -1,9 +1,60 @@
|
|||
# Changelog
|
||||
MLEM uses [semantic versioning](https://semver.org/).
|
||||
MLEM tries to adhere to [semantic versioning](https://semver.org/).
|
||||
|
||||
Jump to version:
|
||||
- [5.1.0](#510)
|
||||
- [5.0.0](#500)
|
||||
|
||||
## 5.1.0
|
||||
### MLEM
|
||||
Additions
|
||||
- Added RotateBy to Direction2Helper
|
||||
|
||||
Improvements
|
||||
- Improved NinePatch memory usage
|
||||
- Moved sound-related classes into Sound namespace
|
||||
- Added customizable overloads for Keybind, Combination and GenericInput ToString methods
|
||||
- Moved ColorHelper.Invert to ColorExtensions.Invert
|
||||
- Removed LINQ Any and All usage in various methods to improve memory usage
|
||||
- Allow enumerating SoundEffectInstanceHandler entries
|
||||
- Improved KeysExtensions memory usage
|
||||
|
||||
Fixes
|
||||
- Set default values for InputHandler held and pressed keys to avoid an exception if buttons are held in the very first frame
|
||||
- Fixed GenericFont MeasureString using incorrect width for Zwsp and OneEmSpace
|
||||
- Fixed tiled NinePatches missing pixels with some scales
|
||||
|
||||
### MLEM.Ui
|
||||
Additions
|
||||
- Added a masking character to TextField to allow for password-style text fields
|
||||
|
||||
Improvements
|
||||
- Removed LINQ Any and All usage in various methods to improve memory usage
|
||||
- Explicitly disallow creating Paragraphs without fonts to make starting out with MLEM.Ui less confusing
|
||||
- Allow adding Link children to non-Paragraph elements
|
||||
|
||||
Fixes
|
||||
- Fixed a crash if a paragraph has a link formatting code, but no font
|
||||
- Fixed tooltips with custom text scale not snapping to the mouse correctly in their first displayed frame
|
||||
- Fixed tooltips not displaying correctly with auto-hiding paragraphs
|
||||
- Fixed rounding errors causing AutoInline elements to be pushed into the next line with some ui scales
|
||||
|
||||
### MLEM.Extended
|
||||
Improvements
|
||||
- Use FontStashSharp's built-in LineHeight property for GenericStashFont
|
||||
|
||||
### MLEM.Data
|
||||
Additions
|
||||
- Added the ability to specify a coordinate offset in data texture atlases
|
||||
|
||||
Improvements
|
||||
- Improved RawContentManager's reader loading and added better exception handling
|
||||
- Improved CopyExtensions construction speed
|
||||
- Improved DynamicEnum caching
|
||||
|
||||
Fixes
|
||||
- Fixed DynamicEnum AddFlag going into an infinite loop
|
||||
|
||||
## 5.0.0
|
||||
### MLEM
|
||||
Additions
|
||||
|
|
|
@ -8,6 +8,7 @@ using MLEM.Extensions;
|
|||
using MLEM.Font;
|
||||
using MLEM.Formatting;
|
||||
using MLEM.Formatting.Codes;
|
||||
using MLEM.Input;
|
||||
using MLEM.Misc;
|
||||
using MLEM.Startup;
|
||||
using MLEM.Textures;
|
||||
|
@ -48,7 +49,10 @@ namespace Demos {
|
|||
RadioTexture = new NinePatch(new TextureRegion(this.testTexture, 16, 0, 8, 8), 3),
|
||||
RadioCheckmark = new TextureRegion(this.testTexture, 32, 0, 8, 8)
|
||||
};
|
||||
var untexturedStyle = new UntexturedStyle(this.SpriteBatch);
|
||||
var untexturedStyle = new UntexturedStyle(this.SpriteBatch) {
|
||||
TextScale = style.TextScale,
|
||||
Font = style.Font
|
||||
};
|
||||
// set the defined style as the current one
|
||||
this.UiSystem.Style = style;
|
||||
// scale every ui up by 5
|
||||
|
@ -73,7 +77,7 @@ namespace Demos {
|
|||
});
|
||||
|
||||
this.root.AddChild(new VerticalSpace(3));
|
||||
this.root.AddChild(new Paragraph(Anchor.AutoLeft, 1, "Note that the default style does not contain any textures or font files and, as such, is quite bland. However, the default style is quite easy to override, as can be seen in the code for this demo."));
|
||||
this.root.AddChild(new Paragraph(Anchor.AutoLeft, 1, "Note that the default style does not contain any textures and, as such, is quite bland. However, the default style is quite easy to override, as can be seen in the code for this demo."));
|
||||
this.root.AddChild(new Button(Anchor.AutoCenter, new Vector2(1, 10), "Change Style") {
|
||||
OnPressed = element => this.UiSystem.Style = this.UiSystem.Style == untexturedStyle ? style : untexturedStyle,
|
||||
PositionOffset = new Vector2(0, 1),
|
||||
|
@ -101,6 +105,13 @@ namespace Demos {
|
|||
this.root.AddChild(new Paragraph(Anchor.AutoCenter, 1, "Numbers-only input:", true));
|
||||
this.root.AddChild(new TextField(Anchor.AutoLeft, new Vector2(1, 10), TextField.OnlyNumbers) {PositionOffset = new Vector2(0, 1)});
|
||||
|
||||
this.root.AddChild(new VerticalSpace(3));
|
||||
this.root.AddChild(new Paragraph(Anchor.AutoCenter, 1, "Password-style input:", true));
|
||||
this.root.AddChild(new TextField(Anchor.AutoLeft, new Vector2(1, 10), text: "secret") {
|
||||
PositionOffset = new Vector2(0, 1),
|
||||
MaskingCharacter = '*'
|
||||
});
|
||||
|
||||
this.root.AddChild(new VerticalSpace(3));
|
||||
this.root.AddChild(new Paragraph(Anchor.AutoLeft, 1, "Zoom in or out:"));
|
||||
this.root.AddChild(new Button(Anchor.AutoLeft, new Vector2(10), "+") {
|
||||
|
@ -131,6 +142,10 @@ namespace Demos {
|
|||
this.root.AddChild(new Button(Anchor.AutoLeft, new Vector2(1, 10), "Toggle Mouse Tooltip") {
|
||||
OnPressed = element => tooltip.IsHidden = !tooltip.IsHidden
|
||||
});
|
||||
var delayed = this.root.AddChild(new Button(Anchor.AutoLeft, new Vector2(1, 10), "Delayed Tooltip") {PositionOffset = new Vector2(0, 1)});
|
||||
delayed.AddTooltip(50, "This tooltip appears with a half second delay!").Delay = TimeSpan.FromSeconds(0.5);
|
||||
var condition = this.root.AddChild(new Button(Anchor.AutoLeft, new Vector2(1, 10), "Hold Ctrl for Tooltip") {PositionOffset = new Vector2(0, 1)});
|
||||
condition.AddTooltip(50, p => this.InputHandler.IsModifierKeyDown(ModifierKey.Control) ? "This tooltip only appears when holding control!" : string.Empty);
|
||||
|
||||
var slider = new Slider(Anchor.AutoLeft, new Vector2(1, 10), 5, 1) {
|
||||
StepPerScroll = 0.01F
|
||||
|
@ -178,7 +193,7 @@ namespace Demos {
|
|||
CoroutineHandler.Start(WobbleProgressBar(bar2));
|
||||
var bar3 = this.root.AddChild(new ProgressBar(Anchor.AutoLeft, new Vector2(8, 30), Direction2.Down, 10) {PositionOffset = new Vector2(0, 1)});
|
||||
CoroutineHandler.Start(WobbleProgressBar(bar3));
|
||||
var bar4 = this.root.AddChild(new ProgressBar(Anchor.AutoInline, new Vector2(8, 30), Direction2.Up, 10) {PositionOffset = new Vector2(1, 1)});
|
||||
var bar4 = this.root.AddChild(new ProgressBar(Anchor.AutoInline, new Vector2(8, 30), Direction2.Up, 10) {PositionOffset = new Vector2(1, 0)});
|
||||
CoroutineHandler.Start(WobbleProgressBar(bar4));
|
||||
|
||||
this.root.AddChild(new VerticalSpace(3));
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<img src="../Media/Logo.svg" width="25%" >
|
||||
![The MLEM logo](https://raw.githubusercontent.com/Ellpeck/MLEM/release/Media/Banner.png)
|
||||
|
||||
**MLEM Library for Extending MonoGame** is an addition to the game framework [MonoGame](https://www.monogame.net/) that provides extension methods, quality of life improvements and additional features like a ui system and easy input handling.
|
||||
|
||||
|
@ -6,9 +6,9 @@
|
|||
- Get it on [NuGet](https://www.nuget.org/packages?q=mlem)
|
||||
- Get prerelease builds on [BaGet](https://nuget.ellpeck.de)
|
||||
- See the source code on [GitHub](https://github.com/Ellpeck/MLEM)
|
||||
- See tutorials and API documentation on this website
|
||||
- Check out [the demos](https://github.com/Ellpeck/MLEM/tree/main/Demos) on [Desktop](https://github.com/Ellpeck/MLEM/tree/main/Demos.DesktopGL) or [Android](https://github.com/Ellpeck/MLEM/tree/main/Demos.Android)
|
||||
- See [the changelog](../CHANGELOG.md) for information on updates
|
||||
- See tutorials and API documentation on [the website](https://mlem.ellpeck.de/)
|
||||
- Check out [the demos](https://github.com/Ellpeck/MLEM/tree/release/Demos) on [Desktop](https://github.com/Ellpeck/MLEM/tree/release/Demos.DesktopGL) or [Android](https://github.com/Ellpeck/MLEM/tree/release/Demos.Android)
|
||||
- See [the changelog](https://mlem.ellpeck.de/CHANGELOG.html) for information on updates
|
||||
|
||||
# Made with MLEM
|
||||
- [A Breath of Spring Air](https://ellpeck.itch.io/a-breath-of-spring-air), a short platformer ([Source](https://git.ellpeck.de/Ellpeck/GreatSpringGameJam))
|
||||
|
@ -20,16 +20,17 @@ If you created a game with the help of MLEM, you can get it added to this list b
|
|||
# Gallery
|
||||
Here are some images that show a couple of MLEM's features.
|
||||
|
||||
MLEM.Ui in action:
|
||||
<img src="../Media/Ui.gif">
|
||||
The [MLEM.Ui](https://mlem.ellpeck.de/articles/ui) demo in action:
|
||||
|
||||
MLEM's text formatting system:
|
||||
<img src="../Media/Formatting.png">
|
||||
![A gif showing various user interface elements from the MLEM.Ui demo](https://raw.githubusercontent.com/Ellpeck/MLEM/release/Media/Ui.gif)
|
||||
|
||||
MLEM's [text formatting system](https://mlem.ellpeck.de/articles/text_formatting), which is compatible with both MLEM.Ui and regular sprite batch rendering:
|
||||
|
||||
![An image showing text with various colors and other formatting](https://raw.githubusercontent.com/Ellpeck/MLEM/release/Media/Formatting.png)
|
||||
|
||||
# Friends of MLEM
|
||||
There are several other NuGet packages and tools that work well in combination with MonoGame and MLEM. Here are some of them:
|
||||
- [Contentless](https://github.com/Ellpeck/Contentless), a tool that removes the need to add assets to the MonoGame Content Pipeline manually
|
||||
- [GameBundle](https://github.com/Ellpeck/GameBundle), a tool that packages MonoGame and other .NET Core applications into several distributable formats
|
||||
- [ButlerDotNet](https://github.com/Ellpeck/ButlerDotNet), a tool that automatically downloads and invokes itch.io's butler
|
||||
- [MonoGame.Extended](https://github.com/craftworkgames/MonoGame.Extended), a package that also provides several additional features for MonoGame
|
||||
- [Coroutine](https://github.com/Ellpeck/Coroutine), a package that implements Unity-style coroutines for any project
|
|
@ -1,7 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Content;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
|
@ -12,12 +11,7 @@ namespace MLEM.Data.Content {
|
|||
/// </summary>
|
||||
public class RawContentManager : ContentManager, IGameComponent {
|
||||
|
||||
private static readonly RawContentReader[] Readers = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.Where(a => !a.IsDynamic)
|
||||
.SelectMany(a => a.GetExportedTypes())
|
||||
.Where(t => t.IsSubclassOf(typeof(RawContentReader)) && !t.IsAbstract)
|
||||
.Select(t => t.GetConstructor(Type.EmptyTypes).Invoke(null))
|
||||
.Cast<RawContentReader>().ToArray();
|
||||
private static List<RawContentReader> readers;
|
||||
|
||||
private readonly List<IDisposable> disposableAssets = new List<IDisposable>();
|
||||
|
||||
|
@ -57,7 +51,11 @@ namespace MLEM.Data.Content {
|
|||
|
||||
private T Read<T>(string assetName, T existing) {
|
||||
var triedFiles = new List<string>();
|
||||
foreach (var reader in Readers.Where(r => r.CanRead(typeof(T)))) {
|
||||
if (readers == null)
|
||||
readers = CollectContentReaders();
|
||||
foreach (var reader in readers) {
|
||||
if (!reader.CanRead(typeof(T)))
|
||||
continue;
|
||||
foreach (var ext in reader.GetFileExtensions()) {
|
||||
var file = Path.Combine(this.RootDirectory, $"{assetName}.{ext}");
|
||||
triedFiles.Add(file);
|
||||
|
@ -89,5 +87,30 @@ namespace MLEM.Data.Content {
|
|||
public void Initialize() {
|
||||
}
|
||||
|
||||
private static List<RawContentReader> CollectContentReaders() {
|
||||
var ret = new List<RawContentReader>();
|
||||
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) {
|
||||
try {
|
||||
if (assembly.IsDynamic)
|
||||
continue;
|
||||
foreach (var type in assembly.GetExportedTypes()) {
|
||||
try {
|
||||
if (type.IsAbstract)
|
||||
continue;
|
||||
if (!type.IsSubclassOf(typeof(RawContentReader)))
|
||||
continue;
|
||||
var inst = type.GetConstructor(Type.EmptyTypes).Invoke(null);
|
||||
ret.Add((RawContentReader) inst);
|
||||
} catch (Exception e) {
|
||||
throw new NotSupportedException($"The type {type} cannot be constructed by a RawContentManager. Does it have a visible parameterless constructor?", e);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
|
@ -9,6 +10,7 @@ namespace MLEM.Data {
|
|||
public static class CopyExtensions {
|
||||
|
||||
private const BindingFlags DefaultFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
|
||||
private static readonly Dictionary<Type, ConstructorInfo> ConstructorCache = new Dictionary<Type, ConstructorInfo>();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a shallow copy of the object and returns it.
|
||||
|
@ -85,9 +87,10 @@ namespace MLEM.Data {
|
|||
}
|
||||
|
||||
private static object Construct(Type t, BindingFlags flags) {
|
||||
if (!ConstructorCache.TryGetValue(t, out var constructor)) {
|
||||
var constructors = t.GetConstructors(flags);
|
||||
// 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
|
||||
if (constructor == null)
|
||||
constructor = t.GetConstructor(flags, null, Type.EmptyTypes, null);
|
||||
|
@ -96,6 +99,8 @@ namespace MLEM.Data {
|
|||
constructor = constructors.FirstOrDefault();
|
||||
if (constructor == null)
|
||||
throw new NullReferenceException($"Type {t} does not have a constructor with the required visibility");
|
||||
ConstructorCache.Add(t, constructor);
|
||||
}
|
||||
return constructor.Invoke(new object[constructor.GetParameters().Length]);
|
||||
}
|
||||
|
||||
|
|
|
@ -9,11 +9,30 @@ using MLEM.Textures;
|
|||
|
||||
namespace MLEM.Data {
|
||||
/// <summary>
|
||||
/// <para>
|
||||
/// This class represents an atlas of <see cref="TextureRegion"/> objects which are loaded from a special texture atlas file.
|
||||
/// To load a data texture atlas, you can use <see cref="DataTextureAtlasExtensions.LoadTextureAtlas"/>.
|
||||
/// To see the structure of a Data Texture Atlas, you can check out the sandbox project: <see href="https://github.com/Ellpeck/MLEM/blob/main/Sandbox/Content/Textures/Furniture.atlas"/>.
|
||||
/// Additionally, if you are using Aseprite, there is a script to automatically populate it: <see href="https://github.com/Ellpeck/MLEM/blob/main/Utilities/Populate%20Data%20Texture%20Atlas.lua"/>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Data texture atlases are designed to be easy to write by hand. Because of this, their structure is very simple.
|
||||
/// Each texture region defined in the atlas consists of its name, followed by a set of possible keywords and their arguments, separated by spaces.
|
||||
/// The <c>loc</c> keyword defines the <see cref="TextureRegion.Area"/> of the texture region as a rectangle whose origin is its top-left corner. It requires four arguments: x, y, width and height of the rectangle.
|
||||
/// The (optional) <c>piv</c> keyword defines the <see cref="TextureRegion.PivotPixels"/> of the texture region. It requires two arguments: x and y. If it is not supplied, the pivot defaults to the top-left corner of the texture region.
|
||||
/// The (optional) <c>off</c> keyword defines an offset that is added onto the location and pivot of this texture region. This is useful when copying and pasting a previously defined texture region to create a second region that has a constant offset. It requires two arguments: The x and y offset.
|
||||
/// </para>
|
||||
/// <example>
|
||||
/// The following entry defines a texture region with the name <c>LongTableRight</c>, whose <see cref="TextureRegion.Area"/> will be a rectangle with X=32, Y=30, Width=64, Height=48, and whose <see cref="TextureRegion.PivotPixels"/> will be a vector with X=80, Y=46.
|
||||
/// <code>
|
||||
/// LongTableRight
|
||||
/// loc 32 30 64 48
|
||||
/// piv 80 46
|
||||
/// </code>
|
||||
/// </example>
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// To see a Data Texture Atlas in action, you can check out the sandbox project: <see href="https://github.com/Ellpeck/MLEM/blob/main/Sandbox/Content/Textures/Furniture.atlas"/>.
|
||||
/// Additionally, if you are using Aseprite, there is a script to automatically populate it: <see href="https://github.com/Ellpeck/MLEM/blob/main/Utilities/Populate%20Data%20Texture%20Atlas.lua"/>.
|
||||
/// </remarks>
|
||||
public class DataTextureAtlas {
|
||||
|
||||
/// <summary>
|
||||
|
@ -56,32 +75,29 @@ namespace MLEM.Data {
|
|||
text = reader.ReadToEnd();
|
||||
var atlas = new DataTextureAtlas(texture);
|
||||
|
||||
// parse each texture region: "<name> loc <u> <v> <w> <h> piv <px> <py>" followed by extra data in the form "key <x> <y>"
|
||||
foreach (Match match in Regex.Matches(text, @"(.+)\W+loc\W+([0-9]+)\W+([0-9]+)\W+([0-9]+)\W+([0-9]+)(?:\W+piv\W+([0-9.]+)\W+([0-9.]+))?(?:\W+(\w+)\W+([0-9.]+)\W+([0-9.]+))*")) {
|
||||
// parse each texture region: "<name> loc <u> <v> <w> <h> [piv <px> <py>] [off <ox> <oy>]"
|
||||
foreach (Match match in Regex.Matches(text, @"(.+)\W+loc\W+([0-9]+)\W+([0-9]+)\W+([0-9]+)\W+([0-9]+)\W*(?:piv\W+([0-9.]+)\W+([0-9.]+))?\W*(?:off\W+([0-9.]+)\W+([0-9.]+))?")) {
|
||||
var name = match.Groups[1].Value.Trim();
|
||||
// offset
|
||||
var off = !match.Groups[8].Success ? Vector2.Zero : new Vector2(
|
||||
float.Parse(match.Groups[8].Value, CultureInfo.InvariantCulture),
|
||||
float.Parse(match.Groups[9].Value, CultureInfo.InvariantCulture));
|
||||
|
||||
// location
|
||||
var loc = new Rectangle(
|
||||
int.Parse(match.Groups[2].Value), int.Parse(match.Groups[3].Value),
|
||||
int.Parse(match.Groups[4].Value), int.Parse(match.Groups[5].Value));
|
||||
loc.Offset(off);
|
||||
|
||||
// pivot
|
||||
var piv = Vector2.Zero;
|
||||
if (match.Groups[6].Success) {
|
||||
piv = new Vector2(
|
||||
var piv = !match.Groups[6].Success ? Vector2.Zero : off + new Vector2(
|
||||
float.Parse(match.Groups[6].Value, CultureInfo.InvariantCulture) - (pivotRelative ? 0 : loc.X),
|
||||
float.Parse(match.Groups[7].Value, CultureInfo.InvariantCulture) - (pivotRelative ? 0 : loc.Y));
|
||||
}
|
||||
|
||||
var region = new TextureRegion(texture, loc) {
|
||||
PivotPixels = piv,
|
||||
Name = name
|
||||
};
|
||||
// additional data
|
||||
if (match.Groups[8].Success) {
|
||||
for (var i = 0; i < match.Groups[8].Captures.Count; i++) {
|
||||
region.SetData(match.Groups[8].Captures[i].Value, new Vector2(
|
||||
float.Parse(match.Groups[9].Captures[i].Value, CultureInfo.InvariantCulture) - (pivotRelative ? 0 : loc.X),
|
||||
float.Parse(match.Groups[10].Captures[i].Value, CultureInfo.InvariantCulture) - (pivotRelative ? 0 : loc.Y)));
|
||||
}
|
||||
}
|
||||
atlas.regions.Add(name, region);
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,9 @@ namespace MLEM.Data {
|
|||
/// A dynamic enum uses <see cref="BigInteger"/> as its underlying type, allowing for an arbitrary number of enum values to be created, even when a <see cref="FlagsAttribute"/>-like structure is used that would only allow for up to 64 values in a regular enum.
|
||||
/// All enum operations including <see cref="And{T}"/>, <see cref="Or{T}"/>, <see cref="Xor{T}"/> and <see cref="Neg{T}"/> are supported and can be implemented in derived classes using operator overloads.
|
||||
/// To create a custom dynamic enum, simply create a class that extends <see cref="DynamicEnum"/>. New values can then be added using <see cref="Add{T}"/>, <see cref="AddValue{T}"/> or <see cref="AddFlag{T}"/>.
|
||||
///
|
||||
/// This class, and its entire concept, are extremely terrible. If you intend on using this, there's probably at least one better solution available.
|
||||
/// Though if, for some weird reason, you need a way to have more than 64 distinct flags, this is a pretty good solution.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// To include enum-like operator overloads in a dynamic enum named MyEnum, the following code can be used:
|
||||
|
@ -27,10 +30,7 @@ namespace MLEM.Data {
|
|||
[JsonConverter(typeof(DynamicEnumConverter))]
|
||||
public abstract class DynamicEnum {
|
||||
|
||||
private static readonly Dictionary<Type, Dictionary<BigInteger, DynamicEnum>> Values = new Dictionary<Type, Dictionary<BigInteger, DynamicEnum>>();
|
||||
private static readonly Dictionary<Type, Dictionary<BigInteger, DynamicEnum>> FlagCache = new Dictionary<Type, Dictionary<BigInteger, DynamicEnum>>();
|
||||
private static readonly Dictionary<Type, Dictionary<string, DynamicEnum>> ParseCache = new Dictionary<Type, Dictionary<string, DynamicEnum>>();
|
||||
|
||||
private static readonly Dictionary<Type, Storage> Storages = new Dictionary<Type, Storage>();
|
||||
private readonly BigInteger value;
|
||||
|
||||
private Dictionary<DynamicEnum, bool> allFlagsCache;
|
||||
|
@ -59,7 +59,6 @@ namespace MLEM.Data {
|
|||
if (this.allFlagsCache == null)
|
||||
this.allFlagsCache = new Dictionary<DynamicEnum, bool>();
|
||||
if (!this.allFlagsCache.TryGetValue(flags, out var ret)) {
|
||||
// & is very memory-intensive, so we cache the return value
|
||||
ret = (GetValue(this) & GetValue(flags)) == GetValue(flags);
|
||||
this.allFlagsCache.Add(flags, ret);
|
||||
}
|
||||
|
@ -76,7 +75,6 @@ namespace MLEM.Data {
|
|||
if (this.anyFlagsCache == null)
|
||||
this.anyFlagsCache = new Dictionary<DynamicEnum, bool>();
|
||||
if (!this.anyFlagsCache.TryGetValue(flags, out var ret)) {
|
||||
// & is very memory-intensive, so we cache the return value
|
||||
ret = (GetValue(this) & GetValue(flags)) != 0;
|
||||
this.anyFlagsCache.Add(flags, ret);
|
||||
}
|
||||
|
@ -107,22 +105,20 @@ namespace MLEM.Data {
|
|||
/// <returns>The newly created enum value</returns>
|
||||
/// <exception cref="ArgumentException">Thrown if the name or value passed are already present</exception>
|
||||
public static T Add<T>(string name, BigInteger value) where T : DynamicEnum {
|
||||
if (!Values.TryGetValue(typeof(T), out var dict)) {
|
||||
dict = new Dictionary<BigInteger, DynamicEnum>();
|
||||
Values.Add(typeof(T), dict);
|
||||
}
|
||||
var storage = GetStorage(typeof(T));
|
||||
|
||||
// cached parsed values and names might be incomplete with new values
|
||||
FlagCache.Remove(typeof(T));
|
||||
ParseCache.Remove(typeof(T));
|
||||
storage.ClearCaches();
|
||||
|
||||
if (dict.ContainsKey(value))
|
||||
if (storage.Values.ContainsKey(value))
|
||||
throw new ArgumentException($"Duplicate value {value}", nameof(value));
|
||||
if (dict.Values.Any(v => v.name == name))
|
||||
foreach (var v in storage.Values.Values) {
|
||||
if (v.name == name)
|
||||
throw new ArgumentException($"Duplicate name {name}", nameof(name));
|
||||
}
|
||||
|
||||
var ret = Construct(typeof(T), name, value);
|
||||
dict.Add(value, ret);
|
||||
storage.Values.Add(value, ret);
|
||||
return (T) ret;
|
||||
}
|
||||
|
||||
|
@ -136,10 +132,8 @@ namespace MLEM.Data {
|
|||
/// <returns>The newly created enum value</returns>
|
||||
public static T AddValue<T>(string name) where T : DynamicEnum {
|
||||
BigInteger value = 0;
|
||||
if (Values.TryGetValue(typeof(T), out var defined)) {
|
||||
while (defined.ContainsKey(value))
|
||||
while (GetStorage(typeof(T)).Values.ContainsKey(value))
|
||||
value++;
|
||||
}
|
||||
return Add<T>(name, value);
|
||||
}
|
||||
|
||||
|
@ -152,11 +146,9 @@ namespace MLEM.Data {
|
|||
/// <typeparam name="T">The type to add this value to</typeparam>
|
||||
/// <returns>The newly created enum value</returns>
|
||||
public static T AddFlag<T>(string name) where T : DynamicEnum {
|
||||
BigInteger value = 0;
|
||||
if (Values.TryGetValue(typeof(T), out var defined)) {
|
||||
while (defined.ContainsKey(value))
|
||||
BigInteger value = 1;
|
||||
while (GetStorage(typeof(T)).Values.ContainsKey(value))
|
||||
value <<= 1;
|
||||
}
|
||||
return Add<T>(name, value);
|
||||
}
|
||||
|
||||
|
@ -177,7 +169,7 @@ namespace MLEM.Data {
|
|||
/// <param name="type">The type whose values to get</param>
|
||||
/// <returns>The defined values for the given type</returns>
|
||||
public static IEnumerable<DynamicEnum> GetValues(Type type) {
|
||||
return Values.TryGetValue(type, out var ret) ? ret.Values : Enumerable.Empty<DynamicEnum>();
|
||||
return GetStorage(type).Values.Values;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -188,7 +180,12 @@ namespace MLEM.Data {
|
|||
/// <typeparam name="T">The type of the values</typeparam>
|
||||
/// <returns>The bitwise OR (|) combination</returns>
|
||||
public static T Or<T>(T left, T right) where T : DynamicEnum {
|
||||
return GetEnumValue<T>(GetValue(left) | GetValue(right));
|
||||
var cache = GetStorage(typeof(T)).OrCache;
|
||||
if (!cache.TryGetValue((left, right), out var ret)) {
|
||||
ret = GetEnumValue<T>(GetValue(left) | GetValue(right));
|
||||
cache.Add((left, right), ret);
|
||||
}
|
||||
return (T) ret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -199,7 +196,12 @@ namespace MLEM.Data {
|
|||
/// <typeparam name="T">The type of the values</typeparam>
|
||||
/// <returns>The bitwise AND (&) combination</returns>
|
||||
public static T And<T>(T left, T right) where T : DynamicEnum {
|
||||
return GetEnumValue<T>(GetValue(left) & GetValue(right));
|
||||
var cache = GetStorage(typeof(T)).AndCache;
|
||||
if (!cache.TryGetValue((left, right), out var ret)) {
|
||||
ret = GetEnumValue<T>(GetValue(left) & GetValue(right));
|
||||
cache.Add((left, right), ret);
|
||||
}
|
||||
return (T) ret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -210,7 +212,12 @@ namespace MLEM.Data {
|
|||
/// <typeparam name="T">The type of the values</typeparam>
|
||||
/// <returns>The bitwise XOR (^) combination</returns>
|
||||
public static T Xor<T>(T left, T right) where T : DynamicEnum {
|
||||
return GetEnumValue<T>(GetValue(left) ^ GetValue(right));
|
||||
var cache = GetStorage(typeof(T)).XorCache;
|
||||
if (!cache.TryGetValue((left, right), out var ret)) {
|
||||
ret = GetEnumValue<T>(GetValue(left) ^ GetValue(right));
|
||||
cache.Add((left, right), ret);
|
||||
}
|
||||
return (T) ret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -220,7 +227,12 @@ namespace MLEM.Data {
|
|||
/// <typeparam name="T">The type of the values</typeparam>
|
||||
/// <returns>The bitwise NEG (~) value</returns>
|
||||
public static T Neg<T>(T value) where T : DynamicEnum {
|
||||
return GetEnumValue<T>(~GetValue(value));
|
||||
var cache = GetStorage(typeof(T)).NegCache;
|
||||
if (!cache.TryGetValue(value, out var ret)) {
|
||||
ret = GetEnumValue<T>(~GetValue(value));
|
||||
cache.Add(value, ret);
|
||||
}
|
||||
return (T) ret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -249,18 +261,16 @@ namespace MLEM.Data {
|
|||
/// <param name="value">The value whose dynamic enum value to get</param>
|
||||
/// <returns>The defined or combined dynamic enum value</returns>
|
||||
public static DynamicEnum GetEnumValue(Type type, BigInteger value) {
|
||||
var storage = GetStorage(type);
|
||||
|
||||
// get the defined value if it exists
|
||||
if (Values.TryGetValue(type, out var values) && values.TryGetValue(value, out var defined))
|
||||
if (storage.Values.TryGetValue(value, out var defined))
|
||||
return defined;
|
||||
|
||||
// otherwise, cache the combined value
|
||||
if (!FlagCache.TryGetValue(type, out var cache)) {
|
||||
cache = new Dictionary<BigInteger, DynamicEnum>();
|
||||
FlagCache.Add(type, cache);
|
||||
}
|
||||
if (!cache.TryGetValue(value, out var combined)) {
|
||||
if (!storage.FlagCache.TryGetValue(value, out var combined)) {
|
||||
combined = Construct(type, null, value);
|
||||
cache.Add(value, combined);
|
||||
storage.FlagCache.Add(value, combined);
|
||||
}
|
||||
return combined;
|
||||
}
|
||||
|
@ -285,10 +295,7 @@ namespace MLEM.Data {
|
|||
/// <param name="strg">The string to parse into a dynamic enum value</param>
|
||||
/// <returns>The parsed enum value, or null if parsing fails</returns>
|
||||
public static DynamicEnum Parse(Type type, string strg) {
|
||||
if (!ParseCache.TryGetValue(type, out var cache)) {
|
||||
cache = new Dictionary<string, DynamicEnum>();
|
||||
ParseCache.Add(type, cache);
|
||||
}
|
||||
var cache = GetStorage(type).ParseCache;
|
||||
if (!cache.TryGetValue(strg, out var cached)) {
|
||||
BigInteger? accum = null;
|
||||
foreach (var val in strg.Split('|')) {
|
||||
|
@ -306,10 +313,39 @@ namespace MLEM.Data {
|
|||
return cached;
|
||||
}
|
||||
|
||||
private static Storage GetStorage(Type type) {
|
||||
if (!Storages.TryGetValue(type, out var storage)) {
|
||||
storage = new Storage();
|
||||
Storages.Add(type, storage);
|
||||
}
|
||||
return storage;
|
||||
}
|
||||
|
||||
private static DynamicEnum Construct(Type type, string name, BigInteger value) {
|
||||
var constructor = type.GetConstructor(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new[] {typeof(string), typeof(BigInteger)}, null);
|
||||
return (DynamicEnum) constructor.Invoke(new object[] {name, value});
|
||||
}
|
||||
|
||||
private class Storage {
|
||||
|
||||
public readonly Dictionary<BigInteger, DynamicEnum> Values = new Dictionary<BigInteger, DynamicEnum>();
|
||||
public readonly Dictionary<BigInteger, DynamicEnum> FlagCache = new Dictionary<BigInteger, DynamicEnum>();
|
||||
public readonly Dictionary<string, DynamicEnum> ParseCache = new Dictionary<string, DynamicEnum>();
|
||||
public readonly Dictionary<(DynamicEnum, DynamicEnum), DynamicEnum> OrCache = new Dictionary<(DynamicEnum, DynamicEnum), DynamicEnum>();
|
||||
public readonly Dictionary<(DynamicEnum, DynamicEnum), DynamicEnum> AndCache = new Dictionary<(DynamicEnum, DynamicEnum), DynamicEnum>();
|
||||
public readonly Dictionary<(DynamicEnum, DynamicEnum), DynamicEnum> XorCache = new Dictionary<(DynamicEnum, DynamicEnum), DynamicEnum>();
|
||||
public readonly Dictionary<DynamicEnum, DynamicEnum> NegCache = new Dictionary<DynamicEnum, DynamicEnum>();
|
||||
|
||||
public void ClearCaches() {
|
||||
this.FlagCache.Clear();
|
||||
this.ParseCache.Clear();
|
||||
this.OrCache.Clear();
|
||||
this.AndCache.Clear();
|
||||
this.XorCache.Clear();
|
||||
this.NegCache.Clear();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -13,6 +13,7 @@
|
|||
<RepositoryUrl>https://github.com/Ellpeck/MLEM</RepositoryUrl>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<PackageIcon>Logo.png</PackageIcon>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -32,5 +33,6 @@
|
|||
|
||||
<ItemGroup>
|
||||
<None Include="../Media/Logo.png" Pack="true" PackagePath="" />
|
||||
<None Include="../Docs/index.md" Pack="true" PackagePath="README.md" />
|
||||
</ItemGroup>
|
||||
</Project>
|
|
@ -33,9 +33,9 @@ namespace MLEM.Extended.Font {
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Vector2 MeasureChar(char c) {
|
||||
protected override float MeasureChar(char c) {
|
||||
var region = this.Font.GetCharacterRegion(c);
|
||||
return region != null ? new Vector2(region.XAdvance, region.Height) : Vector2.Zero;
|
||||
return region != null ? new Vector2(region.XAdvance, region.Height).X : 0;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
|
@ -18,7 +18,7 @@ namespace MLEM.Extended.Font {
|
|||
/// <inheritdoc />
|
||||
public override GenericFont Italic { get; }
|
||||
/// <inheritdoc />
|
||||
public override float LineHeight { get; }
|
||||
public override float LineHeight => this.Font.LineHeight;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new generic font using <see cref="SpriteFontBase"/>.
|
||||
|
@ -29,9 +29,6 @@ namespace MLEM.Extended.Font {
|
|||
/// <param name="italic">An italic version of the font</param>
|
||||
public GenericStashFont(SpriteFontBase font, SpriteFontBase bold = null, SpriteFontBase italic = null) {
|
||||
this.Font = font;
|
||||
// SpriteFontBase provides no line height, so we measure the height of a new line for most fonts
|
||||
// This doesn't work with static sprite fonts, but their size is always the one we calculate here
|
||||
this.LineHeight = font is StaticSpriteFont s ? s.FontSize + s.LineSpacing : font.MeasureString("\n").Y;
|
||||
this.Bold = bold != null ? new GenericStashFont(bold) : this;
|
||||
this.Italic = italic != null ? new GenericStashFont(italic) : this;
|
||||
}
|
||||
|
@ -47,8 +44,8 @@ namespace MLEM.Extended.Font {
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Vector2 MeasureChar(char c) {
|
||||
return this.Font.MeasureString(c.ToCachedString());
|
||||
protected override float MeasureChar(char c) {
|
||||
return this.Font.MeasureString(c.ToCachedString()).X;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
<RepositoryUrl>https://github.com/Ellpeck/MLEM</RepositoryUrl>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<PackageIcon>Logo.png</PackageIcon>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -24,7 +25,7 @@
|
|||
<PackageReference Include="MonoGame.Extended.Tiled" Version="3.8.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="FontStashSharp.MonoGame" Version="0.9.2">
|
||||
<PackageReference Include="FontStashSharp.MonoGame" Version="1.0.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.0.1641">
|
||||
|
@ -34,5 +35,6 @@
|
|||
|
||||
<ItemGroup>
|
||||
<None Include="../Media/Logo.png" Pack="true" PackagePath="" />
|
||||
<None Include="../Docs/index.md" Pack="true" PackagePath="README.md" />
|
||||
</ItemGroup>
|
||||
</Project>
|
|
@ -72,7 +72,15 @@ namespace MLEM.Extended.Tiled {
|
|||
/// <param name="included">A function that determines if a certain info should be included or not</param>
|
||||
/// <returns>An enumerable of collision infos for that area</returns>
|
||||
public IEnumerable<TileCollisionInfo> GetCollidingTiles(RectangleF area, Func<TileCollisionInfo, bool> included = null) {
|
||||
var inclusionFunc = included ?? (tile => tile.Collisions.Any(c => c.Intersects(area)));
|
||||
bool DefaultInclusion(TileCollisionInfo tile) {
|
||||
foreach (var c in tile.Collisions) {
|
||||
if (c.Intersects(area))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
var inclusionFunc = included ?? DefaultInclusion;
|
||||
var minX = Math.Max(0, area.Left.Floor());
|
||||
var maxX = Math.Min(this.map.Width - 1, area.Right.Floor());
|
||||
var minY = Math.Max(0, area.Top.Floor());
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
<RepositoryUrl>https://github.com/Ellpeck/MLEM</RepositoryUrl>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<PackageIcon>Logo.png</PackageIcon>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -28,5 +29,6 @@
|
|||
|
||||
<ItemGroup>
|
||||
<None Include="../Media/Logo.png" Pack="true" PackagePath="" />
|
||||
<None Include="../Docs/index.md" Pack="true" PackagePath="README.md" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
@ -19,12 +19,14 @@
|
|||
<PackageProjectUrl>https://mlem.ellpeck.de/</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/Ellpeck/MLEM</RepositoryUrl>
|
||||
<PackageIcon>Logo.png</PackageIcon>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="content\**\*" Exclude="content\**\.DS_Store;content\**\bin;content\**\obj" />
|
||||
<Compile Remove="**\*" />
|
||||
<None Include="../Media/Logo.png" Pack="true" PackagePath=""/>
|
||||
<None Include="../Docs/index.md" Pack="true" PackagePath="README.md" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -10,6 +10,7 @@ using MLEM.Input;
|
|||
using MLEM.Misc;
|
||||
using MLEM.Textures;
|
||||
using MLEM.Ui.Style;
|
||||
using SoundEffectInfo = MLEM.Sound.SoundEffectInfo;
|
||||
|
||||
namespace MLEM.Ui.Elements {
|
||||
/// <summary>
|
||||
|
@ -606,7 +607,9 @@ namespace MLEM.Ui.Elements {
|
|||
break;
|
||||
case Anchor.AutoInline:
|
||||
var newX = prevArea.Right + this.ScaledOffset.X;
|
||||
if (newX + newSize.X <= parentArea.Right) {
|
||||
// with awkward ui scale values, floating point rounding can cause an element that would usually be
|
||||
// positioned correctly to be pushed into the next line due to a very small deviation, so we add 0.01 here
|
||||
if (newX + newSize.X <= parentArea.Right + 0.01F) {
|
||||
pos.X = newX;
|
||||
pos.Y = prevArea.Y + this.ScaledOffset.Y;
|
||||
} else {
|
||||
|
@ -648,8 +651,8 @@ namespace MLEM.Ui.Elements {
|
|||
autoSize.Y = lowest.UnscrolledArea.Bottom - pos.Y + this.ScaledChildPadding.Bottom;
|
||||
foundChild = lowest;
|
||||
} else {
|
||||
if (this.Children.Count > 0)
|
||||
throw new InvalidOperationException($"{this} with root {this.Root.Name} sets its height based on children but it only has children anchored too low ({string.Join(", ", this.Children.Select(c => c.Anchor))})");
|
||||
if (this.Children.Any(e => !e.IsHidden))
|
||||
throw new InvalidOperationException($"{this} with root {this.Root.Name} sets its height based on children but it only has visible children anchored too low ({string.Join(", ", this.Children.Where(c => !c.IsHidden).Select(c => c.Anchor))})");
|
||||
autoSize.Y = 0;
|
||||
}
|
||||
}
|
||||
|
@ -660,8 +663,8 @@ namespace MLEM.Ui.Elements {
|
|||
autoSize.X = rightmost.UnscrolledArea.Right - pos.X + this.ScaledChildPadding.Right;
|
||||
foundChild = rightmost;
|
||||
} else {
|
||||
if (this.Children.Count > 0)
|
||||
throw new InvalidOperationException($"{this} with root {this.Root.Name} sets its width based on children but it only has children anchored too far right ({string.Join(", ", this.Children.Select(c => c.Anchor))})");
|
||||
if (this.Children.Any(e => !e.IsHidden))
|
||||
throw new InvalidOperationException($"{this} with root {this.Root.Name} sets its width based on children but it only has visible children anchored too far right ({string.Join(", ", this.Children.Where(c => !c.IsHidden).Select(c => c.Anchor))})");
|
||||
autoSize.X = 0;
|
||||
}
|
||||
}
|
||||
|
@ -672,6 +675,7 @@ namespace MLEM.Ui.Elements {
|
|||
autoSize = Vector2.Min(autoSize, actualSize);
|
||||
}
|
||||
|
||||
// we want to leave some leeway to prevent float rounding causing an infinite loop
|
||||
if (!autoSize.Equals(this.UnscrolledArea.Size, 0.01F)) {
|
||||
recursion++;
|
||||
if (recursion >= 16) {
|
||||
|
|
|
@ -131,11 +131,7 @@ namespace MLEM.Ui.Elements {
|
|||
public static Button KeybindButton(Anchor anchor, Vector2 size, Keybind keybind, InputHandler inputHandler, string activePlaceholder, GenericInput unbindKey = default, string unboundPlaceholder = "", Func<GenericInput, string> inputName = null) {
|
||||
string GetCurrentName() {
|
||||
var combination = keybind.GetCombinations().FirstOrDefault();
|
||||
if (combination == null)
|
||||
return unboundPlaceholder;
|
||||
return string.Join(" + ", combination.Modifiers
|
||||
.Append(combination.Key)
|
||||
.Select(i => inputName?.Invoke(i) ?? i.ToString()));
|
||||
return combination?.ToString(" + ", inputName) ?? unboundPlaceholder;
|
||||
}
|
||||
|
||||
var button = new Button(anchor, size, GetCurrentName());
|
||||
|
|
|
@ -151,8 +151,8 @@ namespace MLEM.Ui.Elements {
|
|||
/// <inheritdoc />
|
||||
protected override void InitStyle(UiStyle style) {
|
||||
base.InitStyle(style);
|
||||
this.RegularFont.SetFromStyle(style.Font ?? throw new NotSupportedException("Paragraphs cannot use ui styles that don't have a font. Please supply a custom font by setting UiStyle.Font."));
|
||||
this.TextScale.SetFromStyle(style.TextScale);
|
||||
this.RegularFont.SetFromStyle(style.Font);
|
||||
this.TextColor.SetFromStyle(style.TextColor);
|
||||
}
|
||||
|
||||
|
@ -236,8 +236,11 @@ namespace MLEM.Ui.Elements {
|
|||
/// <inheritdoc />
|
||||
public override void ForceUpdateArea() {
|
||||
// set the position offset and size to the token's first area
|
||||
var area = this.Token.GetArea(Vector2.Zero, this.textScale).First();
|
||||
this.PositionOffset = area.Location + new Vector2(((Paragraph) this.Parent).GetAlignmentOffset() / this.Parent.Scale, 0);
|
||||
var area = this.Token.GetArea(Vector2.Zero, this.textScale).FirstOrDefault();
|
||||
if (this.Parent is Paragraph p)
|
||||
area.Location += new Vector2(p.GetAlignmentOffset() / p.Scale, 0);
|
||||
this.PositionOffset = area.Location;
|
||||
this.IsHidden = area.IsEmpty;
|
||||
this.Size = area.Size;
|
||||
base.ForceUpdateArea();
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
|
@ -24,19 +23,43 @@ namespace MLEM.Ui.Elements {
|
|||
/// <summary>
|
||||
/// A <see cref="Rule"/> that allows any visible character and spaces
|
||||
/// </summary>
|
||||
public static readonly Rule DefaultRule = (field, add) => !add.Any(char.IsControl);
|
||||
public static readonly Rule DefaultRule = (field, add) => {
|
||||
foreach (var c in add) {
|
||||
if (char.IsControl(c))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
/// <summary>
|
||||
/// A <see cref="Rule"/> that only allows letters
|
||||
/// </summary>
|
||||
public static readonly Rule OnlyLetters = (field, add) => add.All(char.IsLetter);
|
||||
public static readonly Rule OnlyLetters = (field, add) => {
|
||||
foreach (var c in add) {
|
||||
if (!char.IsLetter(c))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
/// <summary>
|
||||
/// A <see cref="Rule"/> that only allows numerals
|
||||
/// </summary>
|
||||
public static readonly Rule OnlyNumbers = (field, add) => add.All(char.IsNumber);
|
||||
public static readonly Rule OnlyNumbers = (field, add) => {
|
||||
foreach (var c in add) {
|
||||
if (!char.IsNumber(c))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
/// <summary>
|
||||
/// A <see cref="Rule"/> that only allows letters and numerals
|
||||
/// </summary>
|
||||
public static readonly Rule LettersNumbers = (field, add) => add.All(c => char.IsLetter(c) || char.IsNumber(c));
|
||||
public static readonly Rule LettersNumbers = (field, add) => {
|
||||
foreach (var c in add) {
|
||||
if (!char.IsLetter(c) || !char.IsNumber(c))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
/// <summary>
|
||||
/// A <see cref="Rule"/> that only allows characters not contained in <see cref="Path.GetInvalidPathChars"/>
|
||||
/// </summary>
|
||||
|
@ -74,7 +97,6 @@ namespace MLEM.Ui.Elements {
|
|||
/// The font that this text field should display text with
|
||||
/// </summary>
|
||||
public StyleProp<GenericFont> Font;
|
||||
private readonly StringBuilder text = new StringBuilder();
|
||||
/// <summary>
|
||||
/// This text field's current text
|
||||
/// </summary>
|
||||
|
@ -95,9 +117,6 @@ namespace MLEM.Ui.Elements {
|
|||
/// The width that the caret should render with.
|
||||
/// </summary>
|
||||
public float CaretWidth = 0.5F;
|
||||
private double caretBlinkTimer;
|
||||
private string displayedText;
|
||||
private int textOffset;
|
||||
/// <summary>
|
||||
/// The rule used for text input.
|
||||
/// Rules allow only certain characters to be allowed inside of a text field.
|
||||
|
@ -111,7 +130,6 @@ namespace MLEM.Ui.Elements {
|
|||
/// The description of the <c>KeyboardInput</c> field on mobile devices and consoles
|
||||
/// </summary>
|
||||
public string MobileDescription;
|
||||
private int caretPos;
|
||||
/// <summary>
|
||||
/// The position of the caret within the text.
|
||||
/// This is always between 0 and the <see cref="string.Length"/> of <see cref="Text"/>
|
||||
|
@ -128,6 +146,26 @@ namespace MLEM.Ui.Elements {
|
|||
}
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// A character that should be displayed instead of this text field's <see cref="Text"/> content.
|
||||
/// The amount of masking characters displayed will be equal to the <see cref="Text"/>'s length.
|
||||
/// This behavior is useful for password fields or similar.
|
||||
/// </summary>
|
||||
public char? MaskingCharacter {
|
||||
get => this.maskingCharacter;
|
||||
set {
|
||||
this.maskingCharacter = value;
|
||||
this.HandleTextChange(false);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly StringBuilder text = new StringBuilder();
|
||||
|
||||
private char? maskingCharacter;
|
||||
private double caretBlinkTimer;
|
||||
private string displayedText;
|
||||
private int textOffset;
|
||||
private int caretPos;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new text field with the given settings
|
||||
|
@ -136,18 +174,17 @@ namespace MLEM.Ui.Elements {
|
|||
/// <param name="size">The text field's size</param>
|
||||
/// <param name="rule">The text field's input rule</param>
|
||||
/// <param name="font">The font to use for drawing text</param>
|
||||
public TextField(Anchor anchor, Vector2 size, Rule rule = null, GenericFont font = null) : base(anchor, size) {
|
||||
/// <param name="text">The text that the text field should contain by default</param>
|
||||
public TextField(Anchor anchor, Vector2 size, Rule rule = null, GenericFont font = null, string text = null) : base(anchor, size) {
|
||||
this.InputRule = rule ?? DefaultRule;
|
||||
if (font != null)
|
||||
this.Font.Set(font);
|
||||
if (text != null)
|
||||
this.SetText(text, true);
|
||||
|
||||
MlemPlatform.EnsureExists();
|
||||
this.OnPressed += async e => {
|
||||
var title = this.MobileTitle ?? this.PlaceholderText;
|
||||
var result = await MlemPlatform.Current.OpenOnScreenKeyboard(title, this.MobileDescription, this.Text, false);
|
||||
if (result != null)
|
||||
this.SetText(result.Replace('\n', ' '), true);
|
||||
};
|
||||
|
||||
this.OnPressed += OnPressed;
|
||||
this.OnTextInput += (element, key, character) => {
|
||||
if (!this.IsSelected || this.IsHidden)
|
||||
return;
|
||||
|
@ -164,6 +201,13 @@ namespace MLEM.Ui.Elements {
|
|||
};
|
||||
this.OnDeselected += e => this.CaretPos = 0;
|
||||
this.OnSelected += e => this.CaretPos = this.text.Length;
|
||||
|
||||
async void OnPressed(Element e) {
|
||||
var title = this.MobileTitle ?? this.PlaceholderText;
|
||||
var result = await MlemPlatform.Current.OpenOnScreenKeyboard(title, this.MobileDescription, this.Text, false);
|
||||
if (result != null)
|
||||
this.SetText(result.Replace('\n', ' '), true);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleTextChange(bool textChanged = true) {
|
||||
|
@ -190,6 +234,8 @@ namespace MLEM.Ui.Elements {
|
|||
this.displayedText = this.Text;
|
||||
this.textOffset = 0;
|
||||
}
|
||||
if (this.MaskingCharacter != null)
|
||||
this.displayedText = new string(this.MaskingCharacter.Value, this.displayedText.Length);
|
||||
|
||||
if (textChanged)
|
||||
this.OnTextChange?.Invoke(this, this.Text);
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MLEM.Ui.Style;
|
||||
|
||||
|
@ -25,6 +24,7 @@ namespace MLEM.Ui.Elements {
|
|||
public Paragraph Paragraph;
|
||||
|
||||
private TimeSpan delayCountdown;
|
||||
private bool autoHidden;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new tooltip with the given settings
|
||||
|
@ -56,10 +56,14 @@ namespace MLEM.Ui.Elements {
|
|||
base.Update(time);
|
||||
this.SnapPositionToMouse();
|
||||
|
||||
if (this.IsHidden && this.delayCountdown > TimeSpan.Zero) {
|
||||
if (this.delayCountdown > TimeSpan.Zero) {
|
||||
this.delayCountdown -= time.ElapsedGameTime;
|
||||
if (this.delayCountdown <= TimeSpan.Zero)
|
||||
if (this.delayCountdown <= TimeSpan.Zero) {
|
||||
this.IsHidden = false;
|
||||
this.UpdateAutoHidden();
|
||||
}
|
||||
} else {
|
||||
this.UpdateAutoHidden();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -68,6 +72,7 @@ namespace MLEM.Ui.Elements {
|
|||
if (this.Parent != null)
|
||||
throw new NotSupportedException($"A tooltip shouldn't be the child of another element ({this.Parent})");
|
||||
base.ForceUpdateArea();
|
||||
this.SnapPositionToMouse();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
@ -111,6 +116,7 @@ namespace MLEM.Ui.Elements {
|
|||
this.IsHidden = true;
|
||||
this.delayCountdown = this.Delay;
|
||||
}
|
||||
this.autoHidden = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -127,11 +133,7 @@ namespace MLEM.Ui.Elements {
|
|||
/// </summary>
|
||||
/// <param name="elementToHover">The element that should automatically cause the tooltip to appear and disappear when hovered and not hovered, respectively</param>
|
||||
public void AddToElement(Element elementToHover) {
|
||||
elementToHover.OnMouseEnter += element => {
|
||||
// only display the tooltip if there is anything in it
|
||||
if (this.Children.Any(c => !c.IsHidden))
|
||||
this.Display(element.System, element.GetType().Name + "Tooltip");
|
||||
};
|
||||
elementToHover.OnMouseEnter += element => this.Display(element.System, element.GetType().Name + "Tooltip");
|
||||
elementToHover.OnMouseExit += element => this.Remove();
|
||||
}
|
||||
|
||||
|
@ -148,5 +150,21 @@ namespace MLEM.Ui.Elements {
|
|||
this.AddToElement(elementToHover);
|
||||
}
|
||||
|
||||
private void UpdateAutoHidden() {
|
||||
var shouldBeHidden = true;
|
||||
foreach (var child in this.Children) {
|
||||
if (!child.IsHidden) {
|
||||
shouldBeHidden = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (this.autoHidden != shouldBeHidden) {
|
||||
// only auto-hide if IsHidden wasn't changed manually
|
||||
if (this.IsHidden == this.autoHidden)
|
||||
this.IsHidden = shouldBeHidden;
|
||||
this.autoHidden = shouldBeHidden;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -13,10 +13,11 @@
|
|||
<RepositoryUrl>https://github.com/Ellpeck/MLEM</RepositoryUrl>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<PackageIcon>Logo.png</PackageIcon>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="TextCopy" Version="4.3.0" />
|
||||
<PackageReference Include="TextCopy" Version="4.3.1" />
|
||||
<ProjectReference Include="..\MLEM\MLEM.csproj" />
|
||||
|
||||
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.0.1641">
|
||||
|
@ -26,5 +27,6 @@
|
|||
|
||||
<ItemGroup>
|
||||
<None Include="../Media/Logo.png" Pack="true" PackagePath="" />
|
||||
<None Include="../Docs/index.md" Pack="true" PackagePath="README.md" />
|
||||
</ItemGroup>
|
||||
</Project>
|
|
@ -5,6 +5,7 @@ using MLEM.Formatting;
|
|||
using MLEM.Misc;
|
||||
using MLEM.Textures;
|
||||
using MLEM.Ui.Elements;
|
||||
using SoundEffectInfo = MLEM.Sound.SoundEffectInfo;
|
||||
|
||||
namespace MLEM.Ui.Style {
|
||||
/// <summary>
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
using System.Text;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
using MLEM.Extensions;
|
||||
using MLEM.Font;
|
||||
|
||||
namespace MLEM.Ui.Style {
|
||||
/// <summary>
|
||||
|
@ -37,25 +35,6 @@ namespace MLEM.Ui.Style {
|
|||
this.ProgressBarColor = Color.White;
|
||||
this.ProgressBarProgressPadding = new Vector2(1);
|
||||
this.ProgressBarProgressColor = Color.Red;
|
||||
this.Font = new EmptyFont();
|
||||
}
|
||||
|
||||
private class EmptyFont : GenericFont {
|
||||
|
||||
public override GenericFont Bold => this;
|
||||
public override GenericFont Italic => this;
|
||||
public override float LineHeight => 1;
|
||||
|
||||
protected override Vector2 MeasureChar(char c) {
|
||||
return Vector2.Zero;
|
||||
}
|
||||
|
||||
public override void DrawString(SpriteBatch batch, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth) {
|
||||
}
|
||||
|
||||
public override void DrawString(SpriteBatch batch, StringBuilder text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -465,8 +465,14 @@ namespace MLEM.Ui {
|
|||
this.CanSelectContent = true;
|
||||
};
|
||||
this.OnElementRemoved += e => {
|
||||
if (e.CanBeSelected && !this.Element.GetChildren(regardGrandchildren: true).Any(c => c.CanBeSelected))
|
||||
if (e.CanBeSelected) {
|
||||
// check if removing this element removed all other selectable elements
|
||||
foreach (var c in this.Element.GetChildren(regardGrandchildren: true)) {
|
||||
if (c.CanBeSelected)
|
||||
return;
|
||||
}
|
||||
this.CanSelectContent = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -18,15 +18,8 @@ namespace MLEM.Extensions {
|
|||
return color * (other.A / 255F);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A set of utility methods for dealing with <see cref="Color"/> objects
|
||||
/// </summary>
|
||||
public static class ColorHelper {
|
||||
|
||||
/// <summary>
|
||||
/// Returns an inverted version of the color.
|
||||
/// Returns an inverted version of this color.
|
||||
/// </summary>
|
||||
/// <param name="color">The color to invert</param>
|
||||
/// <returns>The inverted color</returns>
|
||||
|
@ -34,6 +27,13 @@ namespace MLEM.Extensions {
|
|||
return new Color(Math.Abs(255 - color.R), Math.Abs(255 - color.G), Math.Abs(255 - color.B), color.A);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A set of utility methods for dealing with <see cref="Color"/> objects
|
||||
/// </summary>
|
||||
public static class ColorHelper {
|
||||
|
||||
/// <summary>
|
||||
/// Parses a hexadecimal number into an rgba color.
|
||||
/// The number should be in the format <c>0xaarrggbb</c>.
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
using System.Text;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
using MLEM.Extensions;
|
||||
using MLEM.Misc;
|
||||
|
||||
namespace MLEM.Font {
|
||||
|
@ -37,11 +38,19 @@ namespace MLEM.Font {
|
|||
/// </summary>
|
||||
public abstract GenericFont Italic { get; }
|
||||
|
||||
///<inheritdoc cref="SpriteFont.LineSpacing"/>
|
||||
/// <summary>
|
||||
/// The height of each line of text of this font.
|
||||
/// This is the value that the text's draw position is offset by every time a newline character is reached.
|
||||
/// </summary>
|
||||
public abstract float LineHeight { get; }
|
||||
|
||||
///<inheritdoc cref="SpriteFont.MeasureString(string)"/>
|
||||
protected abstract Vector2 MeasureChar(char c);
|
||||
/// <summary>
|
||||
/// Measures the width of the given character with the default scale for use in <see cref="MeasureString"/>.
|
||||
/// Note that this method does not support <see cref="Nbsp"/>, <see cref="Zwsp"/> and <see cref="OneEmSpace"/> for most generic fonts, which is why <see cref="MeasureString"/> should be used even for single characters.
|
||||
/// </summary>
|
||||
/// <param name="c">The character whose width to calculate</param>
|
||||
/// <returns>The width of the given character with the default scale</returns>
|
||||
protected abstract float MeasureChar(char c);
|
||||
|
||||
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
|
||||
public abstract void DrawString(SpriteBatch batch, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth);
|
||||
|
@ -49,27 +58,34 @@ namespace MLEM.Font {
|
|||
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
|
||||
public abstract void DrawString(SpriteBatch batch, StringBuilder text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth);
|
||||
|
||||
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
|
||||
public void DrawString(SpriteBatch batch, string text, Vector2 position, Color color) {
|
||||
this.DrawString(batch, text, position, color, 0, Vector2.Zero, Vector2.One, SpriteEffects.None, 0);
|
||||
}
|
||||
|
||||
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
|
||||
public void DrawString(SpriteBatch batch, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects effects, float layerDepth) {
|
||||
this.DrawString(batch, text, position, color, rotation, origin, new Vector2(scale), effects, layerDepth);
|
||||
}
|
||||
|
||||
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
|
||||
public void DrawString(SpriteBatch batch, StringBuilder text, Vector2 position, Color color) {
|
||||
this.DrawString(batch, text, position, color, 0, Vector2.Zero, Vector2.One, SpriteEffects.None, 0);
|
||||
}
|
||||
|
||||
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
|
||||
public void DrawString(SpriteBatch batch, StringBuilder text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects effects, float layerDepth) {
|
||||
this.DrawString(batch, text, position, color, rotation, origin, new Vector2(scale), effects, layerDepth);
|
||||
}
|
||||
|
||||
///<inheritdoc cref="SpriteFont.MeasureString(string)"/>
|
||||
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
|
||||
public void DrawString(SpriteBatch batch, string text, Vector2 position, Color color) {
|
||||
this.DrawString(batch, text, position, color, 0, Vector2.Zero, Vector2.One, SpriteEffects.None, 0);
|
||||
}
|
||||
|
||||
///<inheritdoc cref="SpriteBatch.DrawString(SpriteFont,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
|
||||
public void DrawString(SpriteBatch batch, StringBuilder text, Vector2 position, Color color) {
|
||||
this.DrawString(batch, text, position, color, 0, Vector2.Zero, Vector2.One, SpriteEffects.None, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Measures the width of the given string when drawn with this font's underlying font.
|
||||
/// This method uses <see cref="MeasureChar"/> internally to calculate the size of known characters and calculates additional characters like <see cref="Nbsp"/>, <see cref="Zwsp"/> and <see cref="OneEmSpace"/>.
|
||||
/// If the text contains newline characters (\n), the size returned will represent a rectangle that encompasses the width of the longest line and the string's full height.
|
||||
/// </summary>
|
||||
/// <param name="text">The text whose size to calculate</param>
|
||||
/// <param name="ignoreTrailingSpaces">Whether trailing whitespace should be ignored in the returned size, causing the end of each line to be effectively trimmed</param>
|
||||
/// <returns>The size of the string when drawn with this font</returns>
|
||||
public Vector2 MeasureString(string text, bool ignoreTrailingSpaces = false) {
|
||||
var size = Vector2.Zero;
|
||||
if (text.Length <= 0)
|
||||
|
@ -85,7 +101,7 @@ namespace MLEM.Font {
|
|||
xOffset += this.LineHeight;
|
||||
break;
|
||||
case Nbsp:
|
||||
xOffset += this.MeasureChar(' ').X;
|
||||
xOffset += this.MeasureChar(' ');
|
||||
break;
|
||||
case Zwsp:
|
||||
// don't add width for a zero-width space
|
||||
|
@ -96,10 +112,10 @@ namespace MLEM.Font {
|
|||
i = text.Length - 1;
|
||||
break;
|
||||
}
|
||||
xOffset += this.MeasureChar(' ').X;
|
||||
xOffset += this.MeasureChar(' ');
|
||||
break;
|
||||
default:
|
||||
xOffset += this.MeasureChar(text[i]).X;
|
||||
xOffset += this.MeasureChar(text[i]);
|
||||
break;
|
||||
}
|
||||
// increase x size if this line is the longest
|
||||
|
@ -164,9 +180,9 @@ namespace MLEM.Font {
|
|||
widthSinceLastSpace = 0;
|
||||
currWidth = 0;
|
||||
} else {
|
||||
var cWidth = this.MeasureChar(c).X * scale;
|
||||
var cWidth = this.MeasureString(c.ToCachedString()).X * scale;
|
||||
if (c == ' ' || c == OneEmSpace || c == Zwsp) {
|
||||
// remember the location of this space
|
||||
// remember the location of this (breaking!) space
|
||||
lastSpaceIndex = ret.Length;
|
||||
widthSinceLastSpace = 0;
|
||||
} else if (currWidth + cWidth >= width) {
|
||||
|
|
|
@ -33,8 +33,8 @@ namespace MLEM.Font {
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Vector2 MeasureChar(char c) {
|
||||
return this.Font.MeasureString(c.ToCachedString());
|
||||
protected override float MeasureChar(char c) {
|
||||
return this.Font.MeasureString(c.ToCachedString()).X;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
|
@ -100,7 +100,13 @@ namespace MLEM.Formatting {
|
|||
/// <param name="scale">The scale that the string is drawn at</param>
|
||||
/// <returns>The token under the target position</returns>
|
||||
public Token GetTokenUnderPos(Vector2 stringPos, Vector2 target, float scale) {
|
||||
return this.Tokens.FirstOrDefault(t => t.GetArea(stringPos, scale).Any(r => r.Contains(target)));
|
||||
foreach (var token in this.Tokens) {
|
||||
foreach (var rect in token.GetArea(stringPos, scale)) {
|
||||
if (rect.Contains(target))
|
||||
return token;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="GenericFont.DrawString(SpriteBatch,string,Vector2,Color,float,Vector2,float,SpriteEffects,float)"/>
|
||||
|
|
|
@ -27,19 +27,16 @@ namespace MLEM.Input {
|
|||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() {
|
||||
var ret = this.Type.ToString();
|
||||
switch (this.Type) {
|
||||
case InputType.Mouse:
|
||||
ret += ((MouseButton) this).ToString();
|
||||
break;
|
||||
return $"Mouse{(MouseButton) this}";
|
||||
case InputType.Keyboard:
|
||||
ret += ((Keys) this).ToString();
|
||||
break;
|
||||
return ((Keys) this).ToString();
|
||||
case InputType.Gamepad:
|
||||
ret += ((Buttons) this).ToString();
|
||||
break;
|
||||
return $"Gamepad{(Buttons) this}";
|
||||
default:
|
||||
return this.Type.ToString();
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
|
@ -120,13 +120,13 @@ namespace MLEM.Input {
|
|||
/// An array of all <see cref="Keys"/>, <see cref="Buttons"/> and <see cref="MouseButton"/> values that are currently down.
|
||||
/// Note that this value only gets set if <see cref="StoreAllActiveInputs"/> is true.
|
||||
/// </summary>
|
||||
public GenericInput[] InputsDown { get; private set; }
|
||||
public GenericInput[] InputsDown { get; private set; } = Array.Empty<GenericInput>();
|
||||
/// <summary>
|
||||
/// An array of all <see cref="Keys"/>, <see cref="Buttons"/> and <see cref="MouseButton"/> that are currently considered pressed.
|
||||
/// An input is considered pressed if it was up in the last update, and is up in the current one.
|
||||
/// Note that this value only gets set if <see cref="StoreAllActiveInputs"/> is true.
|
||||
/// </summary>
|
||||
public GenericInput[] InputsPressed { get; private set; }
|
||||
public GenericInput[] InputsPressed { get; private set; } = Array.Empty<GenericInput>();
|
||||
private readonly List<GenericInput> inputsDownAccum = new List<GenericInput>();
|
||||
/// <summary>
|
||||
/// Set this field to false to enable <see cref="InputsDown"/> and <see cref="InputsPressed"/> being calculated.
|
||||
|
@ -363,7 +363,11 @@ namespace MLEM.Input {
|
|||
/// <param name="modifier">The modifier key</param>
|
||||
/// <returns>If the modifier key is down</returns>
|
||||
public bool IsModifierKeyDown(ModifierKey modifier) {
|
||||
return modifier.GetKeys().Any(this.IsKeyDown);
|
||||
foreach (var key in modifier.GetKeys()) {
|
||||
if (this.IsKeyDown(key))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -570,18 +574,30 @@ namespace MLEM.Input {
|
|||
}
|
||||
|
||||
/// <inheritdoc cref="IsDown"/>
|
||||
public bool IsAnyDown(params GenericInput[] control) {
|
||||
return control.Any(c => this.IsDown(c));
|
||||
public bool IsAnyDown(params GenericInput[] controls) {
|
||||
foreach (var control in controls) {
|
||||
if (this.IsDown(control))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IsUp"/>
|
||||
public bool IsAnyUp(params GenericInput[] control) {
|
||||
return control.Any(c => this.IsUp(c));
|
||||
public bool IsAnyUp(params GenericInput[] controls) {
|
||||
foreach (var control in controls) {
|
||||
if (this.IsUp(control))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IsPressed"/>
|
||||
public bool IsAnyPressed(params GenericInput[] control) {
|
||||
return control.Any(c => this.IsPressed(c));
|
||||
public bool IsAnyPressed(params GenericInput[] controls) {
|
||||
foreach (var control in controls) {
|
||||
if (this.IsPressed(control))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -89,7 +89,11 @@ namespace MLEM.Input {
|
|||
/// <param name="gamepadIndex">The index of the gamepad to query, or -1 to query all gamepads</param>
|
||||
/// <returns>Whether this keybind is considered to be down</returns>
|
||||
public bool IsDown(InputHandler handler, int gamepadIndex = -1) {
|
||||
return this.combinations.Any(c => c.IsDown(handler, gamepadIndex));
|
||||
foreach (var combination in this.combinations) {
|
||||
if (combination.IsDown(handler, gamepadIndex))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -100,7 +104,11 @@ namespace MLEM.Input {
|
|||
/// <param name="gamepadIndex">The index of the gamepad to query, or -1 to query all gamepads</param>
|
||||
/// <returns>Whether this keybind is considered to be pressed</returns>
|
||||
public bool IsPressed(InputHandler handler, int gamepadIndex = -1) {
|
||||
return this.combinations.Any(c => c.IsPressed(handler, gamepadIndex));
|
||||
foreach (var combination in this.combinations) {
|
||||
if (combination.IsPressed(handler, gamepadIndex))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -111,7 +119,11 @@ namespace MLEM.Input {
|
|||
/// <param name="gamepadIndex">The index of the gamepad to query, or -1 to query all gamepads</param>
|
||||
/// <returns>Whether any of this keyboard's modifier keys are down</returns>
|
||||
public bool IsModifierDown(InputHandler handler, int gamepadIndex = -1) {
|
||||
return this.combinations.Any(c => c.IsModifierDown(handler, gamepadIndex));
|
||||
foreach (var combination in this.combinations) {
|
||||
if (combination.IsModifierDown(handler, gamepadIndex))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -123,6 +135,23 @@ namespace MLEM.Input {
|
|||
yield return combination;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts this keybind into an easily human-readable string.
|
||||
/// When using <see cref="ToString()"/>, this method is used with <paramref name="joiner"/> set to ", ".
|
||||
/// </summary>
|
||||
/// <param name="joiner">The string to use to join combinations</param>
|
||||
/// <param name="combinationJoiner">The string to use for combination-internal joining, see <see cref="Combination.ToString(string,System.Func{MLEM.Input.GenericInput,string})"/></param>
|
||||
/// <param name="inputName">The function to use for determining the display name of generic inputs, see <see cref="Combination.ToString(string,System.Func{MLEM.Input.GenericInput,string})"/></param>
|
||||
/// <returns>A human-readable string representing this keybind</returns>
|
||||
public string ToString(string joiner, string combinationJoiner = " + ", Func<GenericInput, string> inputName = null) {
|
||||
return string.Join(joiner, this.combinations.Select(c => c.ToString(combinationJoiner, inputName)));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() {
|
||||
return this.ToString(", ");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A key combination is a combination of a set of modifier keys and a key.
|
||||
/// All of the keys are <see cref="GenericInput"/> instances, so they can be keyboard-, mouse- or gamepad-based.
|
||||
|
@ -181,7 +210,29 @@ namespace MLEM.Input {
|
|||
/// <param name="gamepadIndex">The index of the gamepad to query, or -1 to query all gamepads</param>
|
||||
/// <returns>Whether this combination's modifiers are down</returns>
|
||||
public bool IsModifierDown(InputHandler handler, int gamepadIndex = -1) {
|
||||
return this.Modifiers.Length <= 0 || this.Modifiers.Any(m => handler.IsDown(m, gamepadIndex));
|
||||
if (this.Modifiers.Length <= 0)
|
||||
return true;
|
||||
foreach (var modifier in this.Modifiers) {
|
||||
if (handler.IsDown(modifier, gamepadIndex))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts this combination into an easily human-readable string.
|
||||
/// When using <see cref="ToString()"/>, this method is used with <paramref name="joiner"/> set to " + ".
|
||||
/// </summary>
|
||||
/// <param name="joiner">The string to use to join this combination's <see cref="Modifiers"/> and <see cref="Key"/> together</param>
|
||||
/// <param name="inputName">The function to use for determining the display name of a <see cref="GenericInput"/>. If this is null, the generic input's default <see cref="GenericInput.ToString"/> method is used.</param>
|
||||
/// <returns>A human-readable string representing this combination</returns>
|
||||
public string ToString(string joiner, Func<GenericInput, string> inputName = null) {
|
||||
return string.Join(joiner, this.Modifiers.Append(this.Key).Select(i => inputName?.Invoke(i) ?? i.ToString()));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() {
|
||||
return this.ToString(" + ");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -13,6 +13,14 @@ namespace MLEM.Input {
|
|||
/// All enum values of <see cref="ModifierKey"/>
|
||||
/// </summary>
|
||||
public static readonly ModifierKey[] ModifierKeys = EnumHelper.GetValues<ModifierKey>().ToArray();
|
||||
private static readonly Dictionary<ModifierKey, Keys[]> KeysLookup = new Dictionary<ModifierKey, Keys[]> {
|
||||
{ModifierKey.Shift, new[] {Keys.LeftShift, Keys.RightShift}},
|
||||
{ModifierKey.Control, new[] {Keys.LeftControl, Keys.RightControl}},
|
||||
{ModifierKey.Alt, new[] {Keys.LeftAlt, Keys.RightAlt}}
|
||||
};
|
||||
private static readonly Dictionary<Keys, ModifierKey> ModifiersLookup = KeysLookup
|
||||
.SelectMany(kv => kv.Value.Select(v => (kv.Key, v)))
|
||||
.ToDictionary(kv => kv.Item2, kv => kv.Item1);
|
||||
|
||||
/// <summary>
|
||||
/// Returns all of the keys that the given modifier key represents
|
||||
|
@ -20,20 +28,7 @@ namespace MLEM.Input {
|
|||
/// <param name="modifier">The modifier key</param>
|
||||
/// <returns>All of the keys the modifier key represents</returns>
|
||||
public static IEnumerable<Keys> GetKeys(this ModifierKey modifier) {
|
||||
switch (modifier) {
|
||||
case ModifierKey.Shift:
|
||||
yield return Keys.LeftShift;
|
||||
yield return Keys.RightShift;
|
||||
break;
|
||||
case ModifierKey.Control:
|
||||
yield return Keys.LeftControl;
|
||||
yield return Keys.RightControl;
|
||||
break;
|
||||
case ModifierKey.Alt:
|
||||
yield return Keys.LeftAlt;
|
||||
yield return Keys.RightAlt;
|
||||
break;
|
||||
}
|
||||
return KeysLookup.TryGetValue(modifier, out var keys) ? keys : Enumerable.Empty<Keys>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -43,11 +38,7 @@ namespace MLEM.Input {
|
|||
/// <param name="key">The key to convert to a modifier key</param>
|
||||
/// <returns>The modifier key, or <see cref="ModifierKey.None"/></returns>
|
||||
public static ModifierKey GetModifier(this Keys key) {
|
||||
foreach (var mod in ModifierKeys) {
|
||||
if (GetKeys(mod).Contains(key))
|
||||
return mod;
|
||||
}
|
||||
return ModifierKey.None;
|
||||
return ModifiersLookup.TryGetValue(key, out var mod) ? mod : ModifierKey.None;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="GetModifier(Microsoft.Xna.Framework.Input.Keys)"/>
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
<RepositoryUrl>https://github.com/Ellpeck/MLEM</RepositoryUrl>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<PackageIcon>Logo.png</PackageIcon>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -23,5 +24,6 @@
|
|||
|
||||
<ItemGroup>
|
||||
<None Include="../Media/Logo.png" Pack="true" PackagePath="" />
|
||||
<None Include="../Docs/index.md" Pack="true" PackagePath="README.md" />
|
||||
</ItemGroup>
|
||||
</Project>
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using System.Runtime.Serialization;
|
||||
using Microsoft.Xna.Framework;
|
||||
using static MLEM.Misc.Direction2;
|
||||
|
||||
namespace MLEM.Misc {
|
||||
/// <summary>
|
||||
|
@ -83,7 +84,10 @@ namespace MLEM.Misc {
|
|||
/// <summary>
|
||||
/// All directions except <see cref="Direction2.None"/>
|
||||
/// </summary>
|
||||
public static readonly Direction2[] AllExceptNone = All.Where(dir => dir != Direction2.None).ToArray();
|
||||
public static readonly Direction2[] AllExceptNone = All.Where(dir => dir != None).ToArray();
|
||||
|
||||
private static readonly Direction2[] Clockwise = {Up, UpRight, Right, DownRight, Down, DownLeft, Left, UpLeft};
|
||||
private static readonly Dictionary<Direction2, int> ClockwiseLookup = Clockwise.Select((d, i) => (d, i)).ToDictionary(kv => kv.d, kv => kv.i);
|
||||
|
||||
/// <summary>
|
||||
/// Returns if the given direction is considered an "adjacent" direction.
|
||||
|
@ -92,7 +96,7 @@ namespace MLEM.Misc {
|
|||
/// <param name="dir">The direction to query</param>
|
||||
/// <returns>Whether the direction is adjacent</returns>
|
||||
public static bool IsAdjacent(this Direction2 dir) {
|
||||
return dir == Direction2.Up || dir == Direction2.Right || dir == Direction2.Down || dir == Direction2.Left;
|
||||
return dir == Up || dir == Right || dir == Down || dir == Left;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -101,7 +105,7 @@ namespace MLEM.Misc {
|
|||
/// <param name="dir">The direction to query</param>
|
||||
/// <returns>Whether the direction is diagonal</returns>
|
||||
public static bool IsDiagonal(this Direction2 dir) {
|
||||
return dir == Direction2.UpRight || dir == Direction2.DownRight || dir == Direction2.UpLeft || dir == Direction2.DownLeft;
|
||||
return dir == UpRight || dir == DownRight || dir == UpLeft || dir == DownLeft;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -112,21 +116,21 @@ namespace MLEM.Misc {
|
|||
/// <returns>The direction's offset</returns>
|
||||
public static Point Offset(this Direction2 dir) {
|
||||
switch (dir) {
|
||||
case Direction2.Up:
|
||||
case Up:
|
||||
return new Point(0, -1);
|
||||
case Direction2.Right:
|
||||
case Right:
|
||||
return new Point(1, 0);
|
||||
case Direction2.Down:
|
||||
case Down:
|
||||
return new Point(0, 1);
|
||||
case Direction2.Left:
|
||||
case Left:
|
||||
return new Point(-1, 0);
|
||||
case Direction2.UpRight:
|
||||
case UpRight:
|
||||
return new Point(1, -1);
|
||||
case Direction2.DownRight:
|
||||
case DownRight:
|
||||
return new Point(1, 1);
|
||||
case Direction2.DownLeft:
|
||||
case DownLeft:
|
||||
return new Point(-1, 1);
|
||||
case Direction2.UpLeft:
|
||||
case UpLeft:
|
||||
return new Point(-1, -1);
|
||||
default:
|
||||
return Point.Zero;
|
||||
|
@ -152,24 +156,24 @@ namespace MLEM.Misc {
|
|||
/// <returns>The opposite of the direction</returns>
|
||||
public static Direction2 Opposite(this Direction2 dir) {
|
||||
switch (dir) {
|
||||
case Direction2.Up:
|
||||
return Direction2.Down;
|
||||
case Direction2.Right:
|
||||
return Direction2.Left;
|
||||
case Direction2.Down:
|
||||
return Direction2.Up;
|
||||
case Direction2.Left:
|
||||
return Direction2.Right;
|
||||
case Direction2.UpRight:
|
||||
return Direction2.DownLeft;
|
||||
case Direction2.DownRight:
|
||||
return Direction2.UpLeft;
|
||||
case Direction2.DownLeft:
|
||||
return Direction2.UpRight;
|
||||
case Direction2.UpLeft:
|
||||
return Direction2.DownRight;
|
||||
case Up:
|
||||
return Down;
|
||||
case Right:
|
||||
return Left;
|
||||
case Down:
|
||||
return Up;
|
||||
case Left:
|
||||
return Right;
|
||||
case UpRight:
|
||||
return DownLeft;
|
||||
case DownRight:
|
||||
return UpLeft;
|
||||
case DownLeft:
|
||||
return UpRight;
|
||||
case UpLeft:
|
||||
return DownRight;
|
||||
default:
|
||||
return Direction2.None;
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -190,26 +194,9 @@ namespace MLEM.Misc {
|
|||
/// <param name="fortyFiveDegrees">Whether to rotate by 45 degrees. If this is false, the rotation is 90 degrees instead.</param>
|
||||
/// <returns>The rotated direction</returns>
|
||||
public static Direction2 RotateCw(this Direction2 dir, bool fortyFiveDegrees = false) {
|
||||
switch (dir) {
|
||||
case Direction2.Up:
|
||||
return fortyFiveDegrees ? Direction2.UpRight : Direction2.Right;
|
||||
case Direction2.Right:
|
||||
return fortyFiveDegrees ? Direction2.DownRight : Direction2.Down;
|
||||
case Direction2.Down:
|
||||
return fortyFiveDegrees ? Direction2.DownLeft : Direction2.Left;
|
||||
case Direction2.Left:
|
||||
return fortyFiveDegrees ? Direction2.UpLeft : Direction2.Up;
|
||||
case Direction2.UpRight:
|
||||
return fortyFiveDegrees ? Direction2.Right : Direction2.DownRight;
|
||||
case Direction2.DownRight:
|
||||
return fortyFiveDegrees ? Direction2.Down : Direction2.DownLeft;
|
||||
case Direction2.DownLeft:
|
||||
return fortyFiveDegrees ? Direction2.Left : Direction2.UpLeft;
|
||||
case Direction2.UpLeft:
|
||||
return fortyFiveDegrees ? Direction2.Up : Direction2.UpRight;
|
||||
default:
|
||||
return Direction2.None;
|
||||
}
|
||||
if (!ClockwiseLookup.TryGetValue(dir, out var dirIndex))
|
||||
return None;
|
||||
return Clockwise[(dirIndex + (fortyFiveDegrees ? 1 : 2)) % Clockwise.Length];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -219,26 +206,10 @@ namespace MLEM.Misc {
|
|||
/// <param name="fortyFiveDegrees">Whether to rotate by 45 degrees. If this is false, the rotation is 90 degrees instead.</param>
|
||||
/// <returns>The rotated direction</returns>
|
||||
public static Direction2 RotateCcw(this Direction2 dir, bool fortyFiveDegrees = false) {
|
||||
switch (dir) {
|
||||
case Direction2.Up:
|
||||
return fortyFiveDegrees ? Direction2.UpLeft : Direction2.Left;
|
||||
case Direction2.Right:
|
||||
return fortyFiveDegrees ? Direction2.UpRight : Direction2.Up;
|
||||
case Direction2.Down:
|
||||
return fortyFiveDegrees ? Direction2.DownRight : Direction2.Right;
|
||||
case Direction2.Left:
|
||||
return fortyFiveDegrees ? Direction2.DownLeft : Direction2.Down;
|
||||
case Direction2.UpRight:
|
||||
return fortyFiveDegrees ? Direction2.Up : Direction2.UpLeft;
|
||||
case Direction2.DownRight:
|
||||
return fortyFiveDegrees ? Direction2.Right : Direction2.UpRight;
|
||||
case Direction2.DownLeft:
|
||||
return fortyFiveDegrees ? Direction2.Down : Direction2.DownRight;
|
||||
case Direction2.UpLeft:
|
||||
return fortyFiveDegrees ? Direction2.Left : Direction2.DownLeft;
|
||||
default:
|
||||
return Direction2.None;
|
||||
}
|
||||
if (!ClockwiseLookup.TryGetValue(dir, out var dirIndex))
|
||||
return None;
|
||||
var index = dirIndex - (fortyFiveDegrees ? 1 : 2);
|
||||
return Clockwise[index < 0 ? index + Clockwise.Length : index];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -252,7 +223,7 @@ namespace MLEM.Misc {
|
|||
if (Math.Abs(dir.Angle() - offsetAngle) <= MathHelper.PiOver4 / 2)
|
||||
return dir;
|
||||
}
|
||||
return Direction2.None;
|
||||
return None;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -263,10 +234,30 @@ namespace MLEM.Misc {
|
|||
/// <returns>The vector's direction</returns>
|
||||
public static Direction2 To90Direction(this Vector2 offset) {
|
||||
if (offset.X == 0 && offset.Y == 0)
|
||||
return Direction2.None;
|
||||
return None;
|
||||
if (Math.Abs(offset.X) > Math.Abs(offset.Y))
|
||||
return offset.X > 0 ? Direction2.Right : Direction2.Left;
|
||||
return offset.Y > 0 ? Direction2.Down : Direction2.Up;
|
||||
return offset.X > 0 ? Right : Left;
|
||||
return offset.Y > 0 ? Down : Up;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rotates the given direction by a given reference direction
|
||||
/// </summary>
|
||||
/// <param name="dir">The direction to rotate</param>
|
||||
/// <param name="reference">The direction to rotate by</param>
|
||||
/// <param name="start">The direction to use as the default direction</param>
|
||||
/// <returns>The direction, rotated by the reference direction</returns>
|
||||
public static Direction2 RotateBy(this Direction2 dir, Direction2 reference, Direction2 start = Up) {
|
||||
if (!ClockwiseLookup.TryGetValue(reference, out var refIndex))
|
||||
return None;
|
||||
if (!ClockwiseLookup.TryGetValue(start, out var startIndex))
|
||||
return None;
|
||||
if (!ClockwiseLookup.TryGetValue(dir, out var dirIndex))
|
||||
return None;
|
||||
var diff = refIndex - startIndex;
|
||||
if (diff < 0)
|
||||
diff += Clockwise.Length;
|
||||
return Clockwise[(dirIndex + diff) % Clockwise.Length];
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,59 +1,13 @@
|
|||
using System;
|
||||
using Microsoft.Xna.Framework.Audio;
|
||||
using MLEM.Extensions;
|
||||
|
||||
namespace MLEM.Misc {
|
||||
/// <summary>
|
||||
/// A sound effect info is a wrapper around <see cref="SoundEffect"/> that additionally stores <see cref="Volume"/>, <see cref="Pitch"/> and <see cref="Pan"/> information.
|
||||
/// Additionally, a <see cref="SoundEffectInstance"/> can be created using <see cref="CreateInstance"/>.
|
||||
/// </summary>
|
||||
public class SoundEffectInfo {
|
||||
/// <inheritdoc />
|
||||
[Obsolete("This class has been moved to MLEM.Sound.SoundEffectInfo in 5.1.0")]
|
||||
public class SoundEffectInfo : Sound.SoundEffectInfo {
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="SoundEffect"/> that is played by this info.
|
||||
/// </summary>
|
||||
public readonly SoundEffect Sound;
|
||||
/// <summary>
|
||||
/// Volume, ranging from 0.0 (silence) to 1.0 (full volume). Volume during playback is scaled by SoundEffect.MasterVolume.
|
||||
/// </summary>
|
||||
public float Volume;
|
||||
/// <summary>
|
||||
/// Pitch adjustment, ranging from -1.0 (down an octave) to 0.0 (no change) to 1.0 (up an octave).
|
||||
/// </summary>
|
||||
public float Pitch;
|
||||
/// <summary>
|
||||
/// Pan value ranging from -1.0 (left speaker) to 0.0 (centered), 1.0 (right speaker).
|
||||
/// </summary>
|
||||
public float Pan;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new sound effect info with the given values.
|
||||
/// </summary>
|
||||
/// <param name="sound">The sound to play</param>
|
||||
/// <param name="volume">The volume to play the sound with</param>
|
||||
/// <param name="pitch">The pitch to play the sound with</param>
|
||||
/// <param name="pan">The pan to play the sound with</param>
|
||||
public SoundEffectInfo(SoundEffect sound, float volume = 1, float pitch = 0, float pan = 0) {
|
||||
this.Sound = sound;
|
||||
this.Volume = volume;
|
||||
this.Pitch = pitch;
|
||||
this.Pan = pan;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays this info's <see cref="Sound"/> once.
|
||||
/// </summary>
|
||||
/// <returns>False if more sounds are currently playing than the platform allows</returns>
|
||||
public bool Play() {
|
||||
return this.Sound.Play(this.Volume, this.Pitch, this.Pan);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="SoundEffectInstance"/> with this sound effect info's data.
|
||||
/// </summary>
|
||||
/// <param name="isLooped">The value to set the returned instance's <see cref="SoundEffectInstance.IsLooped"/> to. Defaults to false.</param>
|
||||
/// <returns>A new sound effect instance, with this info's data applied</returns>
|
||||
public SoundEffectInstance CreateInstance(bool isLooped = false) {
|
||||
return this.Sound.CreateInstance(this.Volume, this.Pitch, this.Pan, isLooped);
|
||||
/// <inheritdoc />
|
||||
public SoundEffectInfo(SoundEffect sound, float volume = 1, float pitch = 0, float pan = 0) : base(sound, volume, pitch, pan) {
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,142 +1,14 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Audio;
|
||||
|
||||
namespace MLEM.Misc {
|
||||
/// <summary>
|
||||
/// A simple class that handles automatically removing and disposing <see cref="SoundEffectInstance"/> objects once they are done playing to free up the audio source for new sounds.
|
||||
/// Additionally, a callback can be registered that is invoked when the <see cref="SoundEffectInstance"/> finishes playing.
|
||||
/// Note that an object of this class can be added to a <see cref="Game"/> using its <see cref="GameComponentCollection"/>.
|
||||
/// </summary>
|
||||
public class SoundEffectInstanceHandler : GameComponent, IEnumerable<SoundEffectInstance> {
|
||||
/// <inheritdoc />
|
||||
[Obsolete("This class has been moved to MLEM.Sound.SoundEffectInstanceHandler in 5.1.0")]
|
||||
public class SoundEffectInstanceHandler : Sound.SoundEffectInstanceHandler {
|
||||
|
||||
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>
|
||||
/// <inheritdoc />
|
||||
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 />
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
60
MLEM/Sound/SoundEffectInfo.cs
Normal file
60
MLEM/Sound/SoundEffectInfo.cs
Normal file
|
@ -0,0 +1,60 @@
|
|||
using Microsoft.Xna.Framework.Audio;
|
||||
using MLEM.Extensions;
|
||||
|
||||
namespace MLEM.Sound {
|
||||
/// <summary>
|
||||
/// A sound effect info is a wrapper around <see cref="SoundEffect"/> that additionally stores <see cref="Volume"/>, <see cref="Pitch"/> and <see cref="Pan"/> information.
|
||||
/// Additionally, a <see cref="SoundEffectInstance"/> can be created using <see cref="CreateInstance"/>.
|
||||
/// </summary>
|
||||
public class SoundEffectInfo {
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="SoundEffect"/> that is played by this info.
|
||||
/// </summary>
|
||||
public readonly SoundEffect Sound;
|
||||
/// <summary>
|
||||
/// Volume, ranging from 0.0 (silence) to 1.0 (full volume). Volume during playback is scaled by SoundEffect.MasterVolume.
|
||||
/// </summary>
|
||||
public float Volume;
|
||||
/// <summary>
|
||||
/// Pitch adjustment, ranging from -1.0 (down an octave) to 0.0 (no change) to 1.0 (up an octave).
|
||||
/// </summary>
|
||||
public float Pitch;
|
||||
/// <summary>
|
||||
/// Pan value ranging from -1.0 (left speaker) to 0.0 (centered), 1.0 (right speaker).
|
||||
/// </summary>
|
||||
public float Pan;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new sound effect info with the given values.
|
||||
/// </summary>
|
||||
/// <param name="sound">The sound to play</param>
|
||||
/// <param name="volume">The volume to play the sound with</param>
|
||||
/// <param name="pitch">The pitch to play the sound with</param>
|
||||
/// <param name="pan">The pan to play the sound with</param>
|
||||
public SoundEffectInfo(SoundEffect sound, float volume = 1, float pitch = 0, float pan = 0) {
|
||||
this.Sound = sound;
|
||||
this.Volume = volume;
|
||||
this.Pitch = pitch;
|
||||
this.Pan = pan;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays this info's <see cref="Sound"/> once.
|
||||
/// </summary>
|
||||
/// <returns>False if more sounds are currently playing than the platform allows</returns>
|
||||
public bool Play() {
|
||||
return this.Sound.Play(this.Volume, this.Pitch, this.Pan);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="SoundEffectInstance"/> with this sound effect info's data.
|
||||
/// </summary>
|
||||
/// <param name="isLooped">The value to set the returned instance's <see cref="SoundEffectInstance.IsLooped"/> to. Defaults to false.</param>
|
||||
/// <returns>A new sound effect instance, with this info's data applied</returns>
|
||||
public SoundEffectInstance CreateInstance(bool isLooped = false) {
|
||||
return this.Sound.CreateInstance(this.Volume, this.Pitch, this.Pan, isLooped);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
156
MLEM/Sound/SoundEffectInstanceHandler.cs
Normal file
156
MLEM/Sound/SoundEffectInstanceHandler.cs
Normal file
|
@ -0,0 +1,156 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Audio;
|
||||
|
||||
namespace MLEM.Sound {
|
||||
/// <summary>
|
||||
/// A simple class that handles automatically removing and disposing <see cref="SoundEffectInstance"/> objects once they are done playing to free up the audio source for new sounds.
|
||||
/// Additionally, a callback can be registered that is invoked when the <see cref="SoundEffectInstance"/> finishes playing.
|
||||
/// Note that an object of this class can be added to a <see cref="Game"/> using its <see cref="GameComponentCollection"/>.
|
||||
/// </summary>
|
||||
public class SoundEffectInstanceHandler : GameComponent, IEnumerable<SoundEffectInstanceHandler.Entry> {
|
||||
|
||||
private readonly List<Entry> playingSounds = new List<Entry>();
|
||||
private AudioListener[] listeners;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new sound effect instance handler with the given settings
|
||||
/// </summary>
|
||||
/// <param name="game">The game instance</param>
|
||||
public SoundEffectInstanceHandler(Game game) : base(game) {
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Update()"/>
|
||||
public override void Update(GameTime gameTime) {
|
||||
this.Update();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates this sound effect handler and manages all of the <see cref="SoundEffectInstance"/> objects in it.
|
||||
/// If <see cref="SetListeners"/> has been called, all sounds will additionally be updated in 3D space.
|
||||
/// This should be called each update frame.
|
||||
/// </summary>
|
||||
public void Update() {
|
||||
for (var i = this.playingSounds.Count - 1; i >= 0; i--) {
|
||||
var entry = this.playingSounds[i];
|
||||
if (entry.Instance.IsDisposed || entry.Instance.State == SoundState.Stopped) {
|
||||
entry.Instance.Stop(true);
|
||||
entry.OnStopped?.Invoke(entry.Instance);
|
||||
this.playingSounds.RemoveAt(i);
|
||||
} else {
|
||||
entry.TryApply3D(this.listeners);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the collection <see cref="AudioListener"/> objects that should be listening to the sounds in this handler in 3D space.
|
||||
/// If there are one or more listeners, this handler applies 3d effects to all sound effect instances that have been added to this handler along with an <see cref="AudioEmitter"/> in <see cref="Update(Microsoft.Xna.Framework.GameTime)"/> automatically.
|
||||
/// </summary>
|
||||
public void SetListeners(params AudioListener[] listeners) {
|
||||
this.listeners = listeners;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pauses all of the sound effect instances that are currently playing
|
||||
/// </summary>
|
||||
public void Pause() {
|
||||
foreach (var entry in this.playingSounds)
|
||||
entry.Instance.Pause();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resumes all of the sound effect instances in this handler
|
||||
/// </summary>
|
||||
public void Resume() {
|
||||
foreach (var entry in this.playingSounds)
|
||||
entry.Instance.Resume();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new <see cref="SoundEffectInstance"/> to this handler.
|
||||
/// This also starts playing the instance.
|
||||
/// </summary>
|
||||
/// <param name="instance">The instance to add</param>
|
||||
/// <param name="onStopped">The function that should be invoked when this instance stops playing, defaults to null</param>
|
||||
/// <param name="emitter">An optional audio emitter with which 3d sound can be applied</param>
|
||||
/// <returns>The passed instance, for chaining</returns>
|
||||
public SoundEffectInstance Add(SoundEffectInstance instance, Action<SoundEffectInstance> onStopped = null, AudioEmitter emitter = null) {
|
||||
var entry = new Entry(instance, onStopped, emitter);
|
||||
this.playingSounds.Add(entry);
|
||||
instance.Play();
|
||||
entry.TryApply3D(this.listeners);
|
||||
return instance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new <see cref="Misc.SoundEffectInfo"/> to this handler.
|
||||
/// This also starts playing the created instance.
|
||||
/// </summary>
|
||||
/// <param name="info">The info for which to add a <see cref="SoundEffectInstance"/></param>
|
||||
/// <param name="onStopped">The function that should be invoked when this instance stops playing, defaults to null</param>
|
||||
/// <param name="emitter">An optional audio emitter with which 3d sound can be applied</param>
|
||||
/// <returns>The newly created <see cref="SoundEffectInstance"/></returns>
|
||||
public SoundEffectInstance Add(SoundEffectInfo info, Action<SoundEffectInstance> onStopped = null, AudioEmitter emitter = null) {
|
||||
return this.Add(info.CreateInstance(), onStopped, emitter);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new <see cref="SoundEffect"/> to this handler.
|
||||
/// This also starts playing the created instance.
|
||||
/// </summary>
|
||||
/// <param name="effect">The sound for which to add a <see cref="SoundEffectInstance"/></param>
|
||||
/// <param name="onStopped">The function that should be invoked when this instance stops playing, defaults to null</param>
|
||||
/// <param name="emitter">An optional audio emitter with which 3d sound can be applied</param>
|
||||
/// <returns>The newly created <see cref="SoundEffectInstance"/></returns>
|
||||
public SoundEffectInstance Add(SoundEffect effect, Action<SoundEffectInstance> onStopped = null, AudioEmitter emitter = null) {
|
||||
return this.Add(effect.CreateInstance(), onStopped, emitter);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerator<Entry> GetEnumerator() {
|
||||
return this.playingSounds.GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() {
|
||||
return this.GetEnumerator();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An entry in a <see cref="SoundEffectInstanceHandler"/>.
|
||||
/// Each entry stores the <see cref="SoundEffectInstance"/> that is being played, as well as the additional data passed through <see cref="SoundEffectInstanceHandler.Add(Microsoft.Xna.Framework.Audio.SoundEffectInstance,System.Action{Microsoft.Xna.Framework.Audio.SoundEffectInstance},Microsoft.Xna.Framework.Audio.AudioEmitter)"/>.
|
||||
/// </summary>
|
||||
public readonly struct Entry {
|
||||
|
||||
/// <summary>
|
||||
/// The sound effect instance that this entry is playing
|
||||
/// </summary>
|
||||
public readonly SoundEffectInstance Instance;
|
||||
/// <summary>
|
||||
/// An action that is invoked when this entry's <see cref="Instance"/> is stopped.
|
||||
/// This action is invoked in <see cref="SoundEffectInstanceHandler.Update()"/>.
|
||||
/// </summary>
|
||||
public readonly Action<SoundEffectInstance> OnStopped;
|
||||
/// <summary>
|
||||
/// The <see cref="AudioEmitter"/> that this sound effect instance is linked to.
|
||||
/// If the underlying handler's <see cref="SoundEffectInstanceHandler.SetListeners"/> method has been called, 3D sound will be applied.
|
||||
/// </summary>
|
||||
public readonly AudioEmitter Emitter;
|
||||
|
||||
internal Entry(SoundEffectInstance instance, Action<SoundEffectInstance> onStopped, AudioEmitter emitter) {
|
||||
this.Instance = instance;
|
||||
this.OnStopped = onStopped;
|
||||
this.Emitter = emitter;
|
||||
}
|
||||
|
||||
internal void TryApply3D(AudioListener[] listeners) {
|
||||
if (listeners != null && listeners.Length > 0 && this.Emitter != null)
|
||||
this.Instance.Apply3D(listeners, this.Emitter);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -1,6 +1,4 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
using MLEM.Extensions;
|
||||
|
@ -41,7 +39,9 @@ namespace MLEM.Textures {
|
|||
this.Region = texture;
|
||||
this.Padding = padding;
|
||||
this.Mode = mode;
|
||||
this.SourceRectangles = this.CreateRectangles(this.Region.Area).ToArray();
|
||||
this.SourceRectangles = new Rectangle[9];
|
||||
for (var i = 0; i < this.SourceRectangles.Length; i++)
|
||||
this.SourceRectangles[i] = (Rectangle) this.GetRectangleForIndex((RectangleF) this.Region.Area, i);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -77,7 +77,7 @@ namespace MLEM.Textures {
|
|||
this(texture, padding, padding, padding, padding, mode) {
|
||||
}
|
||||
|
||||
internal IEnumerable<RectangleF> CreateRectangles(RectangleF area, float patchScale = 1) {
|
||||
internal RectangleF GetRectangleForIndex(RectangleF area, int index, float patchScale = 1) {
|
||||
var pl = this.Padding.Left * patchScale;
|
||||
var pr = this.Padding.Right * patchScale;
|
||||
var pt = this.Padding.Top * patchScale;
|
||||
|
@ -90,19 +90,28 @@ namespace MLEM.Textures {
|
|||
var topY = area.Y + pt;
|
||||
var bottomY = area.Y + area.Height - pb;
|
||||
|
||||
yield return new RectangleF(area.X, area.Y, pl, pt);
|
||||
yield return new RectangleF(leftX, area.Y, centerW, pt);
|
||||
yield return new RectangleF(rightX, area.Y, pr, pt);
|
||||
yield return new RectangleF(area.X, topY, pl, centerH);
|
||||
yield return new RectangleF(leftX, topY, centerW, centerH);
|
||||
yield return new RectangleF(rightX, topY, pr, centerH);
|
||||
yield return new RectangleF(area.X, bottomY, pl, pb);
|
||||
yield return new RectangleF(leftX, bottomY, centerW, pb);
|
||||
yield return new RectangleF(rightX, bottomY, pr, pb);
|
||||
switch (index) {
|
||||
case 0:
|
||||
return new RectangleF(area.X, area.Y, pl, pt);
|
||||
case 1:
|
||||
return new RectangleF(leftX, area.Y, centerW, pt);
|
||||
case 2:
|
||||
return new RectangleF(rightX, area.Y, pr, pt);
|
||||
case 3:
|
||||
return new RectangleF(area.X, topY, pl, centerH);
|
||||
case 4:
|
||||
return new RectangleF(leftX, topY, centerW, centerH);
|
||||
case 5:
|
||||
return new RectangleF(rightX, topY, pr, centerH);
|
||||
case 6:
|
||||
return new RectangleF(area.X, bottomY, pl, pb);
|
||||
case 7:
|
||||
return new RectangleF(leftX, bottomY, centerW, pb);
|
||||
case 8:
|
||||
return new RectangleF(rightX, bottomY, pr, pb);
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(index));
|
||||
}
|
||||
|
||||
private IEnumerable<Rectangle> CreateRectangles(Rectangle area, float patchScale = 1) {
|
||||
return this.CreateRectangles((RectangleF) area, patchScale).Select(r => (Rectangle) r);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -143,11 +152,10 @@ namespace MLEM.Textures {
|
|||
/// <param name="layerDepth">The depth</param>
|
||||
/// <param name="patchScale">The scale of each area of the nine patch</param>
|
||||
public static void Draw(this SpriteBatch batch, NinePatch texture, RectangleF destinationRectangle, Color color, float rotation, Vector2 origin, SpriteEffects effects, float layerDepth, float patchScale = 1) {
|
||||
var destinations = texture.CreateRectangles(destinationRectangle, patchScale);
|
||||
var count = 0;
|
||||
foreach (var rect in destinations) {
|
||||
for (var i = 0; i < texture.SourceRectangles.Length; i++) {
|
||||
var rect = texture.GetRectangleForIndex(destinationRectangle, i, patchScale);
|
||||
if (!rect.IsEmpty) {
|
||||
var src = texture.SourceRectangles[count];
|
||||
var src = texture.SourceRectangles[i];
|
||||
switch (texture.Mode) {
|
||||
case NinePatchMode.Stretch:
|
||||
batch.Draw(texture.Region.Texture, rect, src, color, rotation, origin, effects, layerDepth);
|
||||
|
@ -158,13 +166,12 @@ namespace MLEM.Textures {
|
|||
for (var x = 0F; x < rect.Width; x += width) {
|
||||
for (var y = 0F; y < rect.Height; y += height) {
|
||||
var size = new Vector2(Math.Min(rect.Width - x, width), Math.Min(rect.Height - y, height));
|
||||
batch.Draw(texture.Region.Texture, new RectangleF(rect.Location + new Vector2(x, y), size), new Rectangle(src.Location, (size / patchScale).ToPoint()), color, rotation, origin, effects, layerDepth);
|
||||
batch.Draw(texture.Region.Texture, new RectangleF(rect.Location + new Vector2(x, y), size), new Rectangle(src.Location, (size / patchScale).CeilCopy().ToPoint()), color, rotation, origin, effects, layerDepth);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
BIN
Media/Banner.png
Normal file
BIN
Media/Banner.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
15
README.md
15
README.md
|
@ -1,11 +1,11 @@
|
|||
<img src="Media/Logo.png" width="25%" >
|
||||
![The MLEM logo](https://raw.githubusercontent.com/Ellpeck/MLEM/main/Media/Banner.png)
|
||||
|
||||
**MLEM Library for Extending MonoGame** is an addition to the game framework [MonoGame](https://www.monogame.net/) that provides extension methods, quality of life improvements and additional features like a ui system and easy input handling.
|
||||
|
||||
# What next?
|
||||
- Get it on [NuGet](https://www.nuget.org/packages?q=mlem)
|
||||
- Get prerelease builds on [BaGet](https://nuget.ellpeck.de)
|
||||
- See the source code in this repository
|
||||
- See the source code on [GitHub](https://github.com/Ellpeck/MLEM)
|
||||
- See tutorials and API documentation on [the website](https://mlem.ellpeck.de/)
|
||||
- Check out [the demos](https://github.com/Ellpeck/MLEM/tree/main/Demos) on [Desktop](https://github.com/Ellpeck/MLEM/tree/main/Demos.DesktopGL) or [Android](https://github.com/Ellpeck/MLEM/tree/main/Demos.Android)
|
||||
- See [the changelog](https://github.com/Ellpeck/MLEM/blob/main/CHANGELOG.md) for information on updates
|
||||
|
@ -20,16 +20,17 @@ If you created a game with the help of MLEM, you can get it added to this list b
|
|||
# Gallery
|
||||
Here are some images that show a couple of MLEM's features.
|
||||
|
||||
MLEM.Ui in action:
|
||||
<img src="Media/Ui.gif">
|
||||
The [MLEM.Ui](https://mlem.ellpeck.de/articles/ui) demo in action:
|
||||
|
||||
MLEM's text formatting system:
|
||||
<img src="Media/Formatting.png">
|
||||
![A gif showing various user interface elements from the MLEM.Ui demo](https://raw.githubusercontent.com/Ellpeck/MLEM/main/Media/Ui.gif)
|
||||
|
||||
MLEM's [text formatting system](https://mlem.ellpeck.de/articles/text_formatting), which is compatible with both MLEM.Ui and regular sprite batch rendering:
|
||||
|
||||
![An image showing text with various colors and other formatting](https://raw.githubusercontent.com/Ellpeck/MLEM/main/Media/Formatting.png)
|
||||
|
||||
# Friends of MLEM
|
||||
There are several other NuGet packages and tools that work well in combination with MonoGame and MLEM. Here are some of them:
|
||||
- [Contentless](https://github.com/Ellpeck/Contentless), a tool that removes the need to add assets to the MonoGame Content Pipeline manually
|
||||
- [GameBundle](https://github.com/Ellpeck/GameBundle), a tool that packages MonoGame and other .NET Core applications into several distributable formats
|
||||
- [ButlerDotNet](https://github.com/Ellpeck/ButlerDotNet), a tool that automatically downloads and invokes itch.io's butler
|
||||
- [MonoGame.Extended](https://github.com/craftworkgames/MonoGame.Extended), a package that also provides several additional features for MonoGame
|
||||
- [Coroutine](https://github.com/Ellpeck/Coroutine), a package that implements Unity-style coroutines for any project
|
|
@ -11,7 +11,7 @@ with.
|
|||
<!--
|
||||
Modify this string to change the font that will be imported.
|
||||
-->
|
||||
<FontName>Arial</FontName>
|
||||
<FontName>Cadman_Roman.otf</FontName>
|
||||
|
||||
<!--
|
||||
Size is a float value, measured in points. Modify this value to change
|
||||
|
|
|
@ -8,9 +8,11 @@ using Microsoft.Xna.Framework.Input;
|
|||
using MLEM.Cameras;
|
||||
using MLEM.Data;
|
||||
using MLEM.Data.Content;
|
||||
using MLEM.Extended.Extensions;
|
||||
using MLEM.Extended.Font;
|
||||
using MLEM.Extended.Tiled;
|
||||
using MLEM.Extensions;
|
||||
using MLEM.Font;
|
||||
using MLEM.Formatting;
|
||||
using MLEM.Formatting.Codes;
|
||||
using MLEM.Input;
|
||||
|
@ -20,6 +22,7 @@ using MLEM.Textures;
|
|||
using MLEM.Ui;
|
||||
using MLEM.Ui.Elements;
|
||||
using MLEM.Ui.Style;
|
||||
using MonoGame.Extended;
|
||||
using MonoGame.Extended.Tiled;
|
||||
using Group = MLEM.Ui.Elements.Group;
|
||||
|
||||
|
@ -69,14 +72,15 @@ namespace Sandbox {
|
|||
textureData[textureData.FromIndex(textureData.ToIndex(25, 9))] = Color.Yellow;
|
||||
}
|
||||
|
||||
var system = new FontSystem(this.GraphicsDevice, 1024, 1024);
|
||||
var system = new FontSystem();
|
||||
system.AddFont(File.ReadAllBytes("Content/Fonts/Cadman_Roman.otf"));
|
||||
//var font = new GenericSpriteFont(LoadContent<SpriteFont>("Fonts/TestFont"));
|
||||
//var font = new GenericBitmapFont(LoadContent<BitmapFont>("Fonts/Regular"));
|
||||
var font = new GenericStashFont(system.GetFont(32));
|
||||
var spriteFont = new GenericSpriteFont(LoadContent<SpriteFont>("Fonts/TestFont"));
|
||||
this.UiSystem.Style = new UntexturedStyle(this.SpriteBatch) {
|
||||
Font = font,
|
||||
TextScale = 0.1F,
|
||||
TextScale = 0.5F,
|
||||
PanelTexture = new NinePatch(new TextureRegion(tex, 0, 8, 24, 24), 8),
|
||||
ButtonTexture = new NinePatch(new TextureRegion(tex, 24, 8, 16, 16), 4)
|
||||
};
|
||||
|
@ -84,6 +88,14 @@ namespace Sandbox {
|
|||
this.UiSystem.AutoScaleWithScreen = true;
|
||||
this.UiSystem.GlobalScale = 5;
|
||||
|
||||
/*this.OnDraw += (g, time) => {
|
||||
const string strg = "This is a test string\nto test things\n\nMany things are being tested, like the ability\nfor this font to agree\n\nwith newlines";
|
||||
this.SpriteBatch.Begin();
|
||||
spriteFont.DrawString(this.SpriteBatch, strg, new Vector2(600, 100), Color.White, 0, Vector2.Zero, 1, SpriteEffects.None, 0);
|
||||
font.DrawString(this.SpriteBatch, strg, new Vector2(600, 100), Color.White, 0, Vector2.Zero, 2, SpriteEffects.None, 0);
|
||||
this.SpriteBatch.End();
|
||||
};*/
|
||||
|
||||
var panel = new Panel(Anchor.Center, new Vector2(0, 100), Vector2.Zero) {SetWidthBasedOnChildren = true};
|
||||
panel.AddChild(new Button(Anchor.AutoLeft, new Vector2(100, 10)));
|
||||
panel.AddChild(new Button(Anchor.AutoCenter, new Vector2(80, 10)));
|
||||
|
@ -129,7 +141,7 @@ namespace Sandbox {
|
|||
Console.WriteLine("The res is " + res);
|
||||
|
||||
var gradient = this.SpriteBatch.GenerateGradientTexture(Color.Green, Color.Red, Color.Blue, Color.Yellow);
|
||||
this.OnDraw += (game, time) => {
|
||||
/*this.OnDraw += (game, time) => {
|
||||
this.SpriteBatch.Begin();
|
||||
this.SpriteBatch.Draw(this.SpriteBatch.GetBlankTexture(), new Rectangle(640 - 4, 360 - 4, 8, 8), Color.Green);
|
||||
|
||||
|
@ -141,7 +153,7 @@ namespace Sandbox {
|
|||
|
||||
this.SpriteBatch.Draw(gradient, new Rectangle(300, 100, 200, 200), Color.White);
|
||||
this.SpriteBatch.End();
|
||||
};
|
||||
};*/
|
||||
|
||||
var sc = 4;
|
||||
var formatter = new TextFormatter();
|
||||
|
@ -218,6 +230,16 @@ namespace Sandbox {
|
|||
CanBeMoused = false
|
||||
});
|
||||
}
|
||||
var par = loadPanel.AddChild(new Paragraph(Anchor.AutoLeft, 1, "This is another\ntest string\n\nwith many lines\nand many more!"));
|
||||
par.OnUpdated = (e, time) => {
|
||||
GenericFont newFont = Input.IsModifierKeyDown(ModifierKey.Shift) ? spriteFont : font;
|
||||
if (newFont != par.RegularFont.Value) {
|
||||
par.TextScaleMultiplier = newFont == font ? 1 : 0.5F;
|
||||
par.RegularFont = newFont;
|
||||
par.ForceUpdateArea();
|
||||
}
|
||||
};
|
||||
par.OnDrawn = (e, time, batch, a) => batch.DrawRectangle(e.DisplayArea.ToExtended(), Color.Red);
|
||||
this.UiSystem.Add("Load", loadGroup);
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
<PackageReference Include="MonoGame.Extended.Content.Pipeline" Version="3.8.0" />
|
||||
<PackageReference Include="MonoGame.Extended.Tiled" Version="3.8.0" />
|
||||
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.0.1641" />
|
||||
<PackageReference Include="FontStashSharp.MonoGame" Version="0.9.2" />
|
||||
<PackageReference Include="FontStashSharp.MonoGame" Version="1.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
@ -1,14 +1,19 @@
|
|||
SimpleDeskUp
|
||||
loc 0 0 48 32
|
||||
piv 16 16
|
||||
|
||||
SimpleDeskRight
|
||||
loc 48 0 48 32
|
||||
piv 80 16
|
||||
|
||||
Plant
|
||||
loc 96 0 16 32
|
||||
|
||||
LongTableUp
|
||||
loc 0 32 64 48
|
||||
piv 16 48
|
||||
|
||||
LongTableRight
|
||||
loc 64 32 64 48
|
||||
piv 112 48
|
||||
loc 32 30 64 48
|
||||
piv 80 46
|
||||
off 32 2
|
|
@ -15,9 +15,15 @@ namespace Tests {
|
|||
var atlas = DataTextureAtlas.LoadAtlasData(new TextureRegion(texture), game.RawContent, "Texture.atlas");
|
||||
Assert.AreEqual(atlas.Regions.Count(), 5);
|
||||
|
||||
// no added offset
|
||||
var table = atlas["LongTableUp"];
|
||||
Assert.AreEqual(table.Area, new Rectangle(0, 32, 64, 48));
|
||||
Assert.AreEqual(table.PivotPixels, new Vector2(16, 48 - 32));
|
||||
|
||||
// added offset
|
||||
var table2 = atlas["LongTableRight"];
|
||||
Assert.AreEqual(table2.Area, new Rectangle(64, 32, 64, 48));
|
||||
Assert.AreEqual(table2.PivotPixels, new Vector2(112 - 64, 48 - 32));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,22 +1,64 @@
|
|||
using Microsoft.Xna.Framework;
|
||||
using MLEM.Misc;
|
||||
using NUnit.Framework;
|
||||
using static MLEM.Misc.Direction2;
|
||||
|
||||
namespace Tests {
|
||||
public class DirectionTests {
|
||||
|
||||
[Test]
|
||||
public void TestDirections() {
|
||||
Assert.AreEqual(new Vector2(0.5F, 0.5F).ToDirection(), Direction2.DownRight);
|
||||
Assert.AreEqual(new Vector2(0.25F, 0.5F).ToDirection(), Direction2.DownRight);
|
||||
Assert.AreEqual(new Vector2(0.15F, 0.5F).ToDirection(), Direction2.Down);
|
||||
Assert.AreEqual(new Vector2(0.5F, 0.5F).ToDirection(), DownRight);
|
||||
Assert.AreEqual(new Vector2(0.25F, 0.5F).ToDirection(), DownRight);
|
||||
Assert.AreEqual(new Vector2(0.15F, 0.5F).ToDirection(), Down);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test90Directions() {
|
||||
Assert.AreEqual(new Vector2(0.75F, 0.5F).To90Direction(), Direction2.Right);
|
||||
Assert.AreEqual(new Vector2(0.5F, 0.5F).To90Direction(), Direction2.Down);
|
||||
Assert.AreEqual(new Vector2(0.25F, 0.5F).To90Direction(), Direction2.Down);
|
||||
Assert.AreEqual(new Vector2(0.75F, 0.5F).To90Direction(), Right);
|
||||
Assert.AreEqual(new Vector2(0.5F, 0.5F).To90Direction(), Down);
|
||||
Assert.AreEqual(new Vector2(0.25F, 0.5F).To90Direction(), Down);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRotations() {
|
||||
// rotate cw
|
||||
Assert.AreEqual(Up.RotateCw(), Right);
|
||||
Assert.AreEqual(Up.RotateCw(true), UpRight);
|
||||
Assert.AreEqual(Left.RotateCw(), Up);
|
||||
Assert.AreEqual(UpLeft.RotateCw(), UpRight);
|
||||
|
||||
// rotate ccw
|
||||
Assert.AreEqual(Up.RotateCcw(), Left);
|
||||
Assert.AreEqual(Up.RotateCcw(true), UpLeft);
|
||||
Assert.AreEqual(Left.RotateCcw(), Down);
|
||||
Assert.AreEqual(UpLeft.RotateCcw(), DownLeft);
|
||||
|
||||
// rotate 360 degrees
|
||||
foreach (var dir in Direction2Helper.AllExceptNone) {
|
||||
Assert.AreEqual(RotateMultipleTimes(dir, true, false, 4), dir);
|
||||
Assert.AreEqual(RotateMultipleTimes(dir, true, true, 8), dir);
|
||||
Assert.AreEqual(RotateMultipleTimes(dir, false, false, 4), dir);
|
||||
Assert.AreEqual(RotateMultipleTimes(dir, false, true, 8), dir);
|
||||
}
|
||||
|
||||
// rotate by with start Up
|
||||
Assert.AreEqual(Right.RotateBy(Right), Down);
|
||||
Assert.AreEqual(Right.RotateBy(Down), Left);
|
||||
Assert.AreEqual(Right.RotateBy(Left), Up);
|
||||
Assert.AreEqual(Right.RotateBy(Up), Right);
|
||||
|
||||
// rotate by with start Left
|
||||
Assert.AreEqual(Up.RotateBy(Right, Left), Down);
|
||||
Assert.AreEqual(Up.RotateBy(Down, Left), Left);
|
||||
Assert.AreEqual(Up.RotateBy(Left, Left), Up);
|
||||
Assert.AreEqual(Up.RotateBy(Up, Left), Right);
|
||||
}
|
||||
|
||||
private static Direction2 RotateMultipleTimes(Direction2 dir, bool clockwise, bool fortyFiveDegrees, int times) {
|
||||
for (var i = 0; i < times; i++)
|
||||
dir = clockwise ? dir.RotateCw(fortyFiveDegrees) : dir.RotateCcw(fortyFiveDegrees);
|
||||
return dir;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -13,10 +13,13 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.0.1641" />
|
||||
<PackageReference Include="coverlet.collector" Version="3.0.3" />
|
||||
<PackageReference Include="coverlet.collector" Version="3.1.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
|
||||
<PackageReference Include="NunitXml.TestLogger" Version="3.0.107" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
#tool docfx.console&version=2.51.0
|
||||
|
||||
// this is the upcoming version, for prereleases
|
||||
var version = Argument("version", "5.0.0");
|
||||
var version = Argument("version", "5.1.0");
|
||||
var target = Argument("target", "Default");
|
||||
var branch = Argument("branch", "main");
|
||||
var config = Argument("configuration", "Release");
|
||||
|
|
Loading…
Reference in a new issue