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