Archived
forked from RDTKEditor/RDTKEditor
添加对 Core 的直接引用
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
using RhythmBase.Adofai.Utils;
|
||||
using RhythmBase.Exceptions;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
namespace RhythmBase.Adofai.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a beat in the ADLevel.
|
||||
/// </summary>
|
||||
public struct ADBeat : IComparable<ADBeat>, IEquatable<ADBeat>
|
||||
{
|
||||
internal readonly ADLevel? baseLevel => _calculator?.Collection;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the beat only value.
|
||||
/// </summary>
|
||||
public readonly float BeatOnly
|
||||
{
|
||||
get => _beat + 1f;
|
||||
|
||||
set
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the time span.
|
||||
/// </summary>
|
||||
public readonly TimeSpan TimeSpan
|
||||
{
|
||||
get => _timeSpan;
|
||||
|
||||
set
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ADBeat"/> struct with a specified beat.
|
||||
/// </summary>
|
||||
/// <param name="beat">The beat value.</param>
|
||||
public ADBeat(float beat)
|
||||
{
|
||||
this = default;
|
||||
_beat = beat;
|
||||
_isBeatLoaded = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ADBeat"/> struct with a specified time span.
|
||||
/// </summary>
|
||||
/// <param name="timeSpan">The time span value.</param>
|
||||
public ADBeat(TimeSpan timeSpan)
|
||||
{
|
||||
this = default;
|
||||
_timeSpan = timeSpan;
|
||||
_isTimeSpanLoaded = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ADBeat"/> struct with a specified calculator and beat.
|
||||
/// </summary>
|
||||
/// <param name="calculator">The beat calculator.</param>
|
||||
/// <param name="beat">The beat value.</param>
|
||||
public ADBeat(ADBeatCalculator calculator, float beat)
|
||||
{
|
||||
this = default;
|
||||
_calculator = calculator;
|
||||
_beat = beat;
|
||||
_isBeatLoaded = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ADBeat"/> struct with a specified calculator and time span.
|
||||
/// </summary>
|
||||
/// <param name="calculator">The beat calculator.</param>
|
||||
/// <param name="timeSpan">The time span value.</param>
|
||||
/// <exception cref="OverflowException">Thrown when the time span is less than zero.</exception>
|
||||
public ADBeat(ADBeatCalculator calculator, TimeSpan timeSpan)
|
||||
{
|
||||
this = default;
|
||||
if (timeSpan < TimeSpan.Zero)
|
||||
{
|
||||
throw new OverflowException(string.Format("The time must not be less than zero, but {0} is given", timeSpan));
|
||||
}
|
||||
_calculator = calculator;
|
||||
_timeSpan = timeSpan;
|
||||
_isTimeSpanLoaded = true;
|
||||
}
|
||||
/// <summary>
|
||||
/// Construct a beat of the 1st beat from the calculator
|
||||
/// </summary>
|
||||
/// <param name="calculator">Specified calculator.</param>
|
||||
/// <returns>The first beat tied to the level.</returns>
|
||||
public static ADBeat Default(ADBeatCalculator calculator)
|
||||
{
|
||||
ADBeat Default = new(calculator, 1f);
|
||||
return Default;
|
||||
}
|
||||
/// <summary>
|
||||
/// Determine if two beats come from the same level
|
||||
/// </summary>
|
||||
/// <param name="a">A beat.</param>
|
||||
/// <param name="b">Another beat.</param>
|
||||
/// <param name="throw">If true, an exception will be thrown when two beats do not come from the same level.</param>
|
||||
/// <returns></returns>
|
||||
public static bool FromSameLevel(ADBeat a, ADBeat b, bool @throw = false)
|
||||
{
|
||||
bool flag = a.baseLevel.Equals(b.baseLevel);
|
||||
bool FromSameLevel;
|
||||
if (flag)
|
||||
{
|
||||
FromSameLevel = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (@throw)
|
||||
{
|
||||
throw new RhythmBaseException("Beats must come from the same ADLevel.");
|
||||
}
|
||||
FromSameLevel = false;
|
||||
}
|
||||
return FromSameLevel;
|
||||
}
|
||||
/// <summary>
|
||||
/// Determine if two beats are from the same level.
|
||||
/// <br />
|
||||
/// If any of them does not come from any level, it will also return true.
|
||||
/// </summary>
|
||||
/// <param name="a">A beat.</param>
|
||||
/// <param name="b">Another beat.</param>
|
||||
/// <param name="throw">If true, an exception will be thrown when two beats do not come from the same level.</param>
|
||||
/// <returns></returns>
|
||||
public static bool FromSameLevelOrNull(ADBeat a, ADBeat b, bool @throw = false) => a.baseLevel == null || b.baseLevel == null || FromSameLevel(a, b, @throw);
|
||||
|
||||
public readonly bool FromSameLevel(ADBeat b, bool @throw = false) => FromSameLevel(this, b, @throw);
|
||||
/// <summary>
|
||||
/// Determine if two beats are from the same level.
|
||||
/// <br />
|
||||
/// If any of them does not come from any level, it will also return true.
|
||||
/// </summary>
|
||||
/// <param name="b">Another beat.</param>
|
||||
/// <param name="throw">If true, an exception will be thrown when two beats do not come from the same level.</param>
|
||||
/// <returns></returns>
|
||||
public readonly bool FromSameLevelOrNull(ADBeat b, bool @throw = false) => baseLevel == null || b.baseLevel == null || FromSameLevel(b, @throw);
|
||||
/// <summary>
|
||||
/// Returns a new instance of unbinding the level.
|
||||
/// </summary>
|
||||
/// <returns>A new instance of unbinding the level.</returns>
|
||||
public readonly ADBeat WithoutBinding()
|
||||
{
|
||||
ADBeat result = this;
|
||||
result._calculator = null;
|
||||
return result;
|
||||
}
|
||||
|
||||
private readonly void IfNullThrowException()
|
||||
{
|
||||
if (IsEmpty)
|
||||
{
|
||||
throw new InvalidRDBeatException();
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Refresh the cache.
|
||||
/// </summary>
|
||||
public void ResetCache()
|
||||
{
|
||||
float i = BeatOnly;
|
||||
_isTimeSpanLoaded = false;
|
||||
}
|
||||
|
||||
internal void ResetBPM()
|
||||
{
|
||||
_isBeatLoaded = true;
|
||||
_isTimeSpanLoaded = false;
|
||||
_isBpmLoaded = false;
|
||||
}
|
||||
|
||||
internal void ResetCPB() => _isBeatLoaded = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance is empty.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if this instance is empty; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public readonly bool IsEmpty
|
||||
{
|
||||
get
|
||||
{
|
||||
return _calculator == null || (!_isBeatLoaded && !_isTimeSpanLoaded);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public static ADBeat operator +(ADBeat a, float b)
|
||||
{
|
||||
ADBeat result = new(a._calculator, a.BeatOnly + b);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public static ADBeat operator +(ADBeat a, TimeSpan b)
|
||||
{
|
||||
ADBeat result = new(a._calculator, a.TimeSpan + b);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public static ADBeat operator -(ADBeat a, float b)
|
||||
{
|
||||
ADBeat result = new(a._calculator, a.BeatOnly - b);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public static ADBeat operator -(ADBeat a, TimeSpan b)
|
||||
{
|
||||
ADBeat result = new(a._calculator, a.TimeSpan - b);
|
||||
return result;
|
||||
}
|
||||
/// <inheritdoc/>
|
||||
public static bool operator >(ADBeat a, ADBeat b) => FromSameLevel(a, b, true) && a.BeatOnly > b.BeatOnly;
|
||||
/// <inheritdoc/>
|
||||
|
||||
public static bool operator <(ADBeat a, ADBeat b) => FromSameLevel(a, b, true) && a.BeatOnly < b.BeatOnly;
|
||||
/// <inheritdoc/>
|
||||
|
||||
public static bool operator >=(ADBeat a, ADBeat b) => FromSameLevel(a, b, true) && a.BeatOnly >= b.BeatOnly;
|
||||
/// <inheritdoc/>
|
||||
|
||||
public static bool operator <=(ADBeat a, ADBeat b) => FromSameLevel(a, b, true) && a.BeatOnly <= b.BeatOnly;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public static bool operator ==(ADBeat a, ADBeat b) => (FromSameLevel(a, b, true) && a._beat == b._beat) || (a._isTimeSpanLoaded && b._isTimeSpanLoaded && a._timeSpan == b._timeSpan) || a.BeatOnly == b.BeatOnly;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public static bool operator !=(ADBeat a, ADBeat b) => !(a == b);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public readonly int CompareTo(ADBeat other) => checked((int)Math.Round((double)unchecked(_beat - other._beat)));
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override readonly string ToString() => string.Format("[{0}]", BeatOnly);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override readonly bool Equals([NotNull] object obj) => obj.GetType() == typeof(ADBeat) && Equals((obj != null) ? ((ADBeat)obj) : default);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public readonly bool Equals(ADBeat other) => this == other;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override readonly int GetHashCode() => HashCode.Combine(BeatOnly, baseLevel);
|
||||
|
||||
internal ADBeatCalculator? _calculator;
|
||||
|
||||
private bool _isBeatLoaded;
|
||||
|
||||
private bool _isTimeSpanLoaded;
|
||||
|
||||
private bool _isBpmLoaded;
|
||||
|
||||
private float _beat;
|
||||
|
||||
private TimeSpan _timeSpan;
|
||||
|
||||
private float _bpm;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
using Microsoft.VisualBasic.CompilerServices;
|
||||
using Newtonsoft.Json;
|
||||
using RhythmBase.Adofai.Converters;
|
||||
using RhythmBase.Adofai.Events;
|
||||
using RhythmBase.Adofai.Utils;
|
||||
using RhythmBase.Exceptions;
|
||||
using RhythmBase.Settings;
|
||||
namespace RhythmBase.Adofai.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Adofal level.
|
||||
/// </summary>
|
||||
public class ADLevel : ADTileCollection
|
||||
{
|
||||
/// <summary>
|
||||
/// Level settings.
|
||||
/// </summary>
|
||||
public ADSettings Settings { get; set; }
|
||||
/// <summary>
|
||||
/// Level decoration collection.
|
||||
/// </summary>
|
||||
public List<ADBaseEvent> Decorations { get; set; }
|
||||
/// <summary>
|
||||
/// Level file path.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public string Path => _path;
|
||||
/// <summary>
|
||||
/// Level directory path.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public string Directory => System.IO.Path.GetDirectoryName(_path);
|
||||
/// <summary>
|
||||
/// Get all the events of the level.
|
||||
/// </summary>
|
||||
public override IEnumerable<ADBaseEvent> Events
|
||||
{
|
||||
get
|
||||
{
|
||||
foreach (ADBaseEvent tile in base.Events)
|
||||
yield return tile;
|
||||
foreach (ADBaseEvent tile2 in Decorations)
|
||||
yield return tile2;
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// The calculator that comes with the level.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public ADBeatCalculator Calculator { get; }
|
||||
public ADLevel()
|
||||
{
|
||||
Settings = new ADSettings();
|
||||
Decorations = [];
|
||||
Calculator = new ADBeatCalculator(this);
|
||||
}
|
||||
public ADLevel(IEnumerable<ADTile> items)
|
||||
{
|
||||
Settings = new ADSettings();
|
||||
Decorations = [];
|
||||
Calculator = new ADBeatCalculator(this);
|
||||
foreach (ADTile tile in items)
|
||||
Add(tile);
|
||||
}
|
||||
/// <summary>
|
||||
/// The default level within the game.
|
||||
/// </summary>
|
||||
public static ADLevel Default => [];
|
||||
/// <summary>
|
||||
/// Read from file as level.
|
||||
/// Use default input settings.
|
||||
/// Supports .rdlevel, .rdzip, .zip file extension.
|
||||
/// </summary>
|
||||
/// <param name="filepath">File path.</param>
|
||||
/// <returns>An instance of a level that reads from a file.</returns>
|
||||
public static ADLevel Read(string filepath) => Read(filepath, new LevelReadOrWriteSettings());
|
||||
/// <summary>
|
||||
/// Read from file as level.
|
||||
/// Supports .rdlevel, .rdzip, .zip file extension.
|
||||
/// </summary>
|
||||
/// <param name="filepath">File path.</param>
|
||||
/// <param name="settings">Input settings.</param>
|
||||
/// <returns>An instance of a level that reads from a file.</returns>
|
||||
public static ADLevel Read(string filepath, LevelReadOrWriteSettings settings)
|
||||
{
|
||||
JsonSerializer LevelSerializer = new();
|
||||
LevelSerializer.Converters.Add(new ADLevelConverter(filepath, settings));
|
||||
string extension = System.IO.Path.GetExtension(filepath);
|
||||
if (extension != ".adofai")
|
||||
{
|
||||
throw new RhythmBaseException("File not supported.");
|
||||
}
|
||||
return LevelSerializer.Deserialize<ADLevel>(new JsonTextReader(File.OpenText(filepath)))!;
|
||||
}
|
||||
internal string _path;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using RhythmBase.Adofai.Events;
|
||||
using RhythmBase.Components;
|
||||
using RhythmBase.Components.Easing;
|
||||
namespace RhythmBase.Adofai.Components
|
||||
{
|
||||
public class ADSettings
|
||||
{
|
||||
public ADSettings()
|
||||
{
|
||||
RequiredMods = [];
|
||||
}
|
||||
|
||||
public int Version { get; set; }
|
||||
|
||||
public string Artist { get; set; }
|
||||
|
||||
public SpecialArtistTypes SpecialArtistType { get; set; }
|
||||
|
||||
public string ArtistPermission { get; set; }
|
||||
|
||||
public string Song { get; set; }
|
||||
|
||||
public string Author { get; set; }
|
||||
|
||||
public bool SeparateCountdownTime { get; set; }
|
||||
|
||||
public string PreviewImage { get; set; }
|
||||
|
||||
public string PreviewIcon { get; set; }
|
||||
|
||||
public RDColor PreviewIconColor { get; set; }
|
||||
|
||||
public float PreviewSongStart { get; set; }
|
||||
|
||||
public float PreviewSongDuration { get; set; }
|
||||
|
||||
public bool SeizureWarning { get; set; }
|
||||
|
||||
public string LevelDesc { get; set; }
|
||||
|
||||
public string LevelTags { get; set; }
|
||||
|
||||
public string ArtistLinks { get; set; }
|
||||
|
||||
public int Difficulty { get; set; }
|
||||
|
||||
public List<string> RequiredMods { get; set; }
|
||||
|
||||
public string SongFilename { get; set; }
|
||||
|
||||
public float Bpm { get; set; }
|
||||
|
||||
public float Volume { get; set; }
|
||||
|
||||
public float Offset { get; set; }
|
||||
|
||||
public float Pitch { get; set; }
|
||||
|
||||
public string Hitsound { get; set; }
|
||||
|
||||
public float HitsoundVolume { get; set; }
|
||||
|
||||
public float CountdownTicks { get; set; }
|
||||
|
||||
public ADTrackColorTypes TrackColorType { get; set; }
|
||||
|
||||
public RDColor TrackColor { get; set; }
|
||||
|
||||
public RDColor SecondaryTrackColor { get; set; }
|
||||
|
||||
public float TrackColorAnimDuration { get; set; }
|
||||
|
||||
public ADTrackColorPulses TrackColorPulse { get; set; }
|
||||
|
||||
public float TrackPulseLength { get; set; }
|
||||
|
||||
public ADTrackStyles TrackStyle { get; set; }
|
||||
|
||||
public ADTrackAnimationTypes TrackAnimation { get; set; }
|
||||
|
||||
public int BeatsAhead { get; set; }
|
||||
|
||||
public ADTrackDisappearAnimationTypes TrackDisappearAnimation { get; set; }
|
||||
|
||||
public int BeatsBehind { get; set; }
|
||||
|
||||
public RDColor BackgroundColor { get; set; }
|
||||
|
||||
public bool ShowDefaultBGIfNoImage { get; set; }
|
||||
|
||||
public string BgImage { get; set; }
|
||||
|
||||
public RDColor BgImageColor { get; set; }
|
||||
|
||||
public RDPointI Parallax { get; set; }
|
||||
|
||||
public BgDisplayModes BgDisplayMode { get; set; }
|
||||
|
||||
public bool LockRot { get; set; }
|
||||
|
||||
public bool LoopBG { get; set; }
|
||||
|
||||
public float ScalingRatio { get; set; }
|
||||
|
||||
public ADCameraRelativeTo RelativeTo { get; set; }
|
||||
|
||||
public RDPointI Position { get; set; }
|
||||
|
||||
public float Rotation { get; set; }
|
||||
|
||||
public float Zoom { get; set; }
|
||||
|
||||
public string BgVideo { get; set; }
|
||||
|
||||
public bool LoopVideo { get; set; }
|
||||
|
||||
public int VidOffset { get; set; }
|
||||
|
||||
public bool FloorIconOutlines { get; set; }
|
||||
|
||||
public bool StickToFloors { get; set; }
|
||||
|
||||
public EaseType PlanetEase { get; set; }
|
||||
|
||||
public int PlanetEaseParts { get; set; }
|
||||
|
||||
public ADEasePartBehaviors PlanetEasePartBehavior { get; set; }
|
||||
|
||||
public RDColor DefaultTextColor { get; set; }
|
||||
|
||||
public RDColor DefaultTextShadowColor { get; set; }
|
||||
|
||||
public string CongratsText { get; set; }
|
||||
|
||||
public string PerfectText { get; set; }
|
||||
|
||||
public bool LegacyFlash { get; set; }
|
||||
|
||||
public bool LegacyCamRelativeTo { get; set; }
|
||||
|
||||
public bool LegacySpriteTiles { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using RhythmBase.Adofai.Events;
|
||||
using System.Collections;
|
||||
namespace RhythmBase.Adofai.Components
|
||||
{
|
||||
public abstract class ADTileCollection : ICollection<ADTile>
|
||||
{
|
||||
protected ADTileCollection()
|
||||
{
|
||||
tileOrder = [];
|
||||
IsReadOnly = false;
|
||||
EndTile = [];
|
||||
}
|
||||
public int Count => tileOrder.Count;
|
||||
public bool IsReadOnly { get; }
|
||||
public ADTile EndTile { get; }
|
||||
public ADTile this[int index] => index == tileOrder.Count ? EndTile : tileOrder[index];
|
||||
public virtual IEnumerable<ADBaseEvent> Events
|
||||
{
|
||||
get
|
||||
{
|
||||
foreach (ADTile tile in tileOrder)
|
||||
foreach (ADBaseTileEvent action in tile)
|
||||
yield return action;
|
||||
foreach (ADBaseTileEvent action2 in EndTile)
|
||||
yield return action2;
|
||||
}
|
||||
}
|
||||
public void Add(ADTile item) => tileOrder.Add(item);
|
||||
public void Clear() => tileOrder.Clear();
|
||||
public void CopyTo(ADTile[] array, int arrayIndex) => tileOrder.CopyTo(array, arrayIndex);
|
||||
public bool Contains(ADTile item) => tileOrder.Contains(item);
|
||||
public bool Remove(ADTile item) => tileOrder.Remove(item);
|
||||
public IEnumerator<ADTile> GetEnumerator() => tileOrder.GetEnumerator();
|
||||
/// <summary>
|
||||
/// Get the index of tile.
|
||||
/// </summary>
|
||||
/// <param name="item">The index of tile.</param>
|
||||
/// <returns></returns>
|
||||
public int IndexOf(ADTile item) => item == EndTile ? Count : tileOrder.IndexOf(item);
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
internal List<ADTile> tileOrder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
namespace RhythmBase.Adofai.Components
|
||||
{
|
||||
public enum ADTrackAnimationTypes
|
||||
{
|
||||
None,
|
||||
Assemble,
|
||||
Assemble_Far,
|
||||
Extend,
|
||||
Grow,
|
||||
Grow_Spin,
|
||||
Fade,
|
||||
Drop,
|
||||
Rise
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
namespace RhythmBase.Adofai.Components
|
||||
{
|
||||
public enum ADTrackDisappearAnimationTypes
|
||||
{
|
||||
None,
|
||||
Scatter,
|
||||
Scatter_Far,
|
||||
Retract,
|
||||
Shrink,
|
||||
Shrink_Spin,
|
||||
Fade
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using RhythmBase.Adofai.Events;
|
||||
using System.Collections;
|
||||
namespace RhythmBase.Adofai.Components
|
||||
{
|
||||
public class ADTypedList<TEvent> : IEnumerable<TEvent> where TEvent : ADBaseEvent
|
||||
{
|
||||
public ADTypedList()
|
||||
{
|
||||
list = [];
|
||||
_types = [];
|
||||
}
|
||||
public void Add(TEvent item)
|
||||
{
|
||||
list.Add(item);
|
||||
_types.Add(item.Type);
|
||||
}
|
||||
public object Remove(TEvent item)
|
||||
{
|
||||
_types.Remove(item.Type);
|
||||
return list.Remove(item);
|
||||
}
|
||||
public override string ToString() => string.Format("Count = {0}", list.Count);
|
||||
public IEnumerator<TEvent> GetEnumerator() => list.GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => list.GetEnumerator();
|
||||
private readonly List<TEvent> list;
|
||||
protected internal HashSet<ADEventType> _types;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using System;
|
||||
namespace RhythmBase.Adofai.Components
|
||||
{
|
||||
public enum BgDisplayModes
|
||||
{
|
||||
FitToScreen,
|
||||
Unscaled,
|
||||
Tiled
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user