1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-18 02:09:52 +08:00

Compare commits

...

406 Commits

253 changed files with 4223 additions and 1721 deletions
-1
View File
@@ -339,6 +339,5 @@ inspectcode
# Fody (pulled in by Realm) - schema file
FodyWeavers.xsd
**/FodyWeavers.xml
.idea/.idea.osu.Desktop/.idea/misc.xml
+1 -5
View File
@@ -8,13 +8,9 @@
<!-- NullabilityInfoContextSupport is disabled by default for Android -->
<NullabilityInfoContextSupport>true</NullabilityInfoContextSupport>
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
<AndroidManifestMerger>manifestmerger.jar</AndroidManifestMerger>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.715.0" />
</ItemGroup>
<ItemGroup>
<AndroidManifestOverlay Include="$(MSBuildThisFileDirectory)osu.Android\Properties\AndroidManifestOverlay.xml" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.812.0" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.
@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="mailto" />
</intent>
</queries>
</manifest>
-3
View File
@@ -54,9 +54,6 @@ namespace osu.Desktop
client.OnReady += onReady;
// safety measure for now, until we performance test / improve backoff for failed connections.
client.OnConnectionFailed += (_, _) => client.Deinitialize();
client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Code} {e.Message}", LoggingTarget.Network);
config.BindWith(OsuSetting.DiscordRichPresence, privacyMode);
@@ -75,7 +75,7 @@ namespace osu.Desktop.LegacyIpc
case LegacyIpcDifficultyCalculationRequest req:
try
{
WorkingBeatmap beatmap = new FlatFileWorkingBeatmap(req.BeatmapFile);
WorkingBeatmap beatmap = new FlatWorkingBeatmap(req.BeatmapFile);
var ruleset = beatmap.BeatmapInfo.Ruleset.CreateInstance();
Mod[] mods = ruleset.ConvertFromLegacyMods((LegacyMods)req.Mods).ToArray();
+1 -1
View File
@@ -26,7 +26,7 @@
<PackageReference Include="Clowd.Squirrel" Version="2.9.42" />
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
<PackageReference Include="System.IO.Packaging" Version="7.0.0" />
<PackageReference Include="DiscordRichPresence" Version="1.1.3.18" />
<PackageReference Include="DiscordRichPresence" Version="1.1.4.20" />
</ItemGroup>
<ItemGroup Label="Resources">
<EmbeddedResource Include="lazer.ico" />
+2 -2
View File
@@ -5,7 +5,7 @@
<key>CFBundleName</key>
<string>osu.Game.Rulesets.Catch.Tests.iOS</string>
<key>CFBundleIdentifier</key>
<string>ppy.osu-Game-Rulesets-Catch-Tests-iOS</string>
<string>sh.ppy.catch-ruleset-tests</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
@@ -42,4 +42,4 @@
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
</dict>
</plist>
</plist>
+2 -2
View File
@@ -5,7 +5,7 @@
<key>CFBundleName</key>
<string>osu.Game.Rulesets.Mania.Tests.iOS</string>
<key>CFBundleIdentifier</key>
<string>ppy.osu-Game-Rulesets-Mania-Tests-iOS</string>
<string>sh.ppy.mania-ruleset-tests</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
@@ -42,4 +42,4 @@
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
</dict>
</plist>
</plist>
@@ -0,0 +1,22 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Screens.Edit.Setup;
namespace osu.Game.Rulesets.Mania.Edit.Setup
{
public partial class ManiaDifficultySection : DifficultySection
{
[BackgroundDependencyLoader]
private void load()
{
CircleSizeSlider.Label = BeatmapsetsStrings.ShowStatsCsMania;
CircleSizeSlider.Description = "The number of columns in the beatmap";
if (CircleSizeSlider.Current is BindableNumber<float> circleSizeFloat)
circleSizeFloat.Precision = 1;
}
}
}
+2
View File
@@ -425,6 +425,8 @@ namespace osu.Game.Rulesets.Mania
}
public override RulesetSetupSection CreateEditorSetupSection() => new ManiaSetupSection();
public override DifficultySection CreateEditorDifficultySection() => new ManiaDifficultySection();
}
public enum PlayfieldType
@@ -9,6 +9,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Primitives;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects;
@@ -25,6 +26,22 @@ namespace osu.Game.Rulesets.Mania.UI
private readonly List<Stage> stages = new List<Stage>();
public override Quad SkinnableComponentScreenSpaceDrawQuad
{
get
{
if (Stages.Count == 1)
return Stages.First().ScreenSpaceDrawQuad;
RectangleF area = RectangleF.Empty;
foreach (var stage in Stages)
area = RectangleF.Union(area, stage.ScreenSpaceDrawQuad.AABBFloat);
return area;
}
}
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => stages.Any(s => s.ReceivePositionalInputAt(screenSpacePos));
public ManiaPlayfield(List<StageDefinition> stageDefinitions)
+2 -2
View File
@@ -5,7 +5,7 @@
<key>CFBundleName</key>
<string>osu.Game.Rulesets.Osu.Tests.iOS</string>
<key>CFBundleIdentifier</key>
<string>ppy.osu-Game-Rulesets-Osu-Tests-iOS</string>
<string>sh.ppy.osu-ruleset-tests</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
@@ -42,4 +42,4 @@
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
</dict>
</plist>
</plist>
@@ -25,6 +25,35 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
[Test]
public void TestSelectAfterFadedOut()
{
var slider = new Slider
{
StartTime = 0,
Position = new Vector2(100, 100),
Path = new SliderPath
{
ControlPoints =
{
new PathControlPoint(),
new PathControlPoint(new Vector2(100))
}
}
};
AddStep("add slider", () => EditorBeatmap.Add(slider));
moveMouseToObject(() => slider);
AddStep("seek after end", () => EditorClock.Seek(750));
AddStep("left click", () => InputManager.Click(MouseButton.Left));
AddAssert("slider not selected", () => EditorBeatmap.SelectedHitObjects.Count == 0);
AddStep("seek to visible", () => EditorClock.Seek(650));
AddStep("left click", () => InputManager.Click(MouseButton.Left));
AddUntilStep("slider selected", () => EditorBeatmap.SelectedHitObjects.Single() == slider);
}
[Test]
public void TestContextMenuShownCorrectlyForSelectedSlider()
{
@@ -0,0 +1,147 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Replays;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
{
public partial class TestSceneSpinnerJudgement : RateAdjustedBeatmapTestScene
{
private const double time_spinner_start = 2000;
private const double time_spinner_end = 4000;
private List<JudgementResult> judgementResults = new List<JudgementResult>();
private ScoreAccessibleReplayPlayer currentPlayer = null!;
[Test]
public void TestHitNothing()
{
performTest(new List<ReplayFrame>());
AddAssert("all min judgements", () => judgementResults.All(result => result.Type == result.Judgement.MinResult));
}
[TestCase(1)]
[TestCase(2)]
[TestCase(5)]
public void TestNumberOfSpins(int spins)
{
performTest(generateReplay(spins));
for (int i = 0; i < spins; ++i)
assertResult<SpinnerTick>(i, HitResult.SmallBonus);
assertResult<SpinnerTick>(spins, HitResult.IgnoreMiss);
}
[Test]
public void TestHitEverything()
{
performTest(generateReplay(20));
AddAssert("all max judgements", () => judgementResults.All(result => result.Type == result.Judgement.MaxResult));
}
private static List<ReplayFrame> generateReplay(int spins)
{
var replayFrames = new List<ReplayFrame>();
const int frames_per_spin = 30;
for (int i = 0; i < spins * frames_per_spin; ++i)
{
float totalProgress = i / (float)(spins * frames_per_spin);
float spinProgress = (i % frames_per_spin) / (float)frames_per_spin;
double time = time_spinner_start + (time_spinner_end - time_spinner_start) * totalProgress;
float posX = MathF.Cos(2 * MathF.PI * spinProgress);
float posY = MathF.Sin(2 * MathF.PI * spinProgress);
Vector2 finalPos = OsuPlayfield.BASE_SIZE / 2 + new Vector2(posX, posY) * 50;
replayFrames.Add(new OsuReplayFrame(time, finalPos, OsuAction.LeftButton));
}
return replayFrames;
}
private void performTest(List<ReplayFrame> frames)
{
AddStep("load player", () =>
{
Beatmap.Value = CreateWorkingBeatmap(new Beatmap<OsuHitObject>
{
HitObjects =
{
new Spinner
{
StartTime = time_spinner_start,
EndTime = time_spinner_end,
Position = OsuPlayfield.BASE_SIZE / 2
}
},
BeatmapInfo =
{
Difficulty = new BeatmapDifficulty(),
Ruleset = new OsuRuleset().RulesetInfo
},
});
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
p.OnLoadComplete += _ =>
{
p.ScoreProcessor.NewJudgement += result =>
{
if (currentPlayer == p) judgementResults.Add(result);
};
};
LoadScreen(currentPlayer = p);
judgementResults = new List<JudgementResult>();
});
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
}
private void assertResult<T>(int index, HitResult expectedResult)
{
AddAssert($"{typeof(T).ReadableName()} ({index}) judged as {expectedResult}",
() => judgementResults.Where(j => j.HitObject is T).OrderBy(j => j.HitObject.StartTime).ElementAt(index).Type,
() => Is.EqualTo(expectedResult));
}
private partial class ScoreAccessibleReplayPlayer : ReplayPlayer
{
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
protected override bool PauseOnFocusLost => false;
public ScoreAccessibleReplayPlayer(Score score)
: base(score, new PlayerConfiguration
{
AllowPause = false,
ShowResults = false,
})
{
}
}
}
}
@@ -22,7 +22,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
protected override bool AlwaysShowWhenSelected => true;
protected override bool ShouldBeAlive => base.ShouldBeAlive
|| (DrawableObject is not DrawableSpinner && ShowHitMarkers.Value && editorClock.CurrentTime >= Item.StartTime && editorClock.CurrentTime - Item.GetEndTime() < HitCircleOverlapMarker.FADE_OUT_EXTENSION);
|| (DrawableObject is not DrawableSpinner && ShowHitMarkers.Value && editorClock.CurrentTime >= Item.StartTime
&& editorClock.CurrentTime - Item.GetEndTime() < HitCircleOverlapMarker.FADE_OUT_EXTENSION);
public override bool IsSelectable =>
// Bypass fade out extension from hit markers for selection purposes.
// This is to match stable, where even when the afterimage hit markers are still visible, objects are not selectable.
base.ShouldBeAlive;
protected OsuSelectionBlueprint(T hitObject)
: base(hitObject)
@@ -62,12 +62,7 @@ namespace osu.Game.Rulesets.Osu.Edit
private void load()
{
// Give a bit of breathing room around the playfield content.
PlayfieldContentContainer.Padding = new MarginPadding
{
Vertical = 10,
Left = TOOLBOX_CONTRACTED_SIZE_LEFT + 10,
Right = TOOLBOX_CONTRACTED_SIZE_RIGHT + 10,
};
PlayfieldContentContainer.Padding = new MarginPadding(10);
LayerBelowRuleset.AddRange(new Drawable[]
{
@@ -17,6 +17,7 @@ using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Utils;
using osuTK;
using osuTK.Input;
@@ -27,11 +28,6 @@ namespace osu.Game.Rulesets.Osu.Edit
[Resolved(CanBeNull = true)]
private IDistanceSnapProvider? snapProvider { get; set; }
/// <summary>
/// During a transform, the initial origin is stored so it can be used throughout the operation.
/// </summary>
private Vector2? referenceOrigin;
/// <summary>
/// During a transform, the initial path types of a single selected slider are stored so they
/// can be maintained throughout the operation.
@@ -42,9 +38,8 @@ namespace osu.Game.Rulesets.Osu.Edit
{
base.OnSelectionChanged();
Quad quad = selectedMovableObjects.Length > 0 ? getSurroundingQuad(selectedMovableObjects) : new Quad();
Quad quad = selectedMovableObjects.Length > 0 ? GeometryUtils.GetSurroundingQuad(selectedMovableObjects) : new Quad();
SelectionBox.CanRotate = quad.Width > 0 || quad.Height > 0;
SelectionBox.CanFlipX = SelectionBox.CanScaleX = quad.Width > 0;
SelectionBox.CanFlipY = SelectionBox.CanScaleY = quad.Height > 0;
SelectionBox.CanReverse = EditorBeatmap.SelectedHitObjects.Count > 1 || EditorBeatmap.SelectedHitObjects.Any(s => s is Slider);
@@ -53,7 +48,6 @@ namespace osu.Game.Rulesets.Osu.Edit
protected override void OnOperationEnded()
{
base.OnOperationEnded();
referenceOrigin = null;
referencePathTypes = null;
}
@@ -109,13 +103,13 @@ namespace osu.Game.Rulesets.Osu.Edit
{
var hitObjects = selectedMovableObjects;
var flipQuad = flipOverOrigin ? new Quad(0, 0, OsuPlayfield.BASE_SIZE.X, OsuPlayfield.BASE_SIZE.Y) : getSurroundingQuad(hitObjects);
var flipQuad = flipOverOrigin ? new Quad(0, 0, OsuPlayfield.BASE_SIZE.X, OsuPlayfield.BASE_SIZE.Y) : GeometryUtils.GetSurroundingQuad(hitObjects);
bool didFlip = false;
foreach (var h in hitObjects)
{
var flippedPosition = GetFlippedPosition(direction, flipQuad, h.Position);
var flippedPosition = GeometryUtils.GetFlippedPosition(direction, flipQuad, h.Position);
if (!Precision.AlmostEquals(flippedPosition, h.Position))
{
@@ -169,34 +163,13 @@ namespace osu.Game.Rulesets.Osu.Edit
if ((reference & Anchor.y0) > 0) scale.Y = -scale.Y;
}
public override bool HandleRotation(float delta)
{
var hitObjects = selectedMovableObjects;
Quad quad = getSurroundingQuad(hitObjects);
referenceOrigin ??= quad.Centre;
foreach (var h in hitObjects)
{
h.Position = RotatePointAroundOrigin(h.Position, referenceOrigin.Value, delta);
if (h is IHasPath path)
{
foreach (PathControlPoint cp in path.Path.ControlPoints)
cp.Position = RotatePointAroundOrigin(cp.Position, Vector2.Zero, delta);
}
}
// this isn't always the case but let's be lenient for now.
return true;
}
public override SelectionRotationHandler CreateRotationHandler() => new OsuSelectionRotationHandler();
private void scaleSlider(Slider slider, Vector2 scale)
{
referencePathTypes ??= slider.Path.ControlPoints.Select(p => p.Type).ToList();
Quad sliderQuad = GetSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position));
Quad sliderQuad = GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position));
// Limit minimum distance between control points after scaling to almost 0. Less than 0 causes the slider to flip, exactly 0 causes a crash through division by 0.
scale = Vector2.ComponentMax(new Vector2(Precision.FLOAT_EPSILON), sliderQuad.Size + scale) - sliderQuad.Size;
@@ -222,7 +195,7 @@ namespace osu.Game.Rulesets.Osu.Edit
slider.SnapTo(snapProvider);
//if sliderhead or sliderend end up outside playfield, revert scaling.
Quad scaledQuad = getSurroundingQuad(new OsuHitObject[] { slider });
Quad scaledQuad = GeometryUtils.GetSurroundingQuad(new OsuHitObject[] { slider });
(bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad);
if (xInBounds && yInBounds && slider.Path.HasValidLength)
@@ -238,10 +211,10 @@ namespace osu.Game.Rulesets.Osu.Edit
private void scaleHitObjects(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale)
{
scale = getClampedScale(hitObjects, reference, scale);
Quad selectionQuad = getSurroundingQuad(hitObjects);
Quad selectionQuad = GeometryUtils.GetSurroundingQuad(hitObjects);
foreach (var h in hitObjects)
h.Position = GetScaledPosition(reference, scale, selectionQuad, h.Position);
h.Position = GeometryUtils.GetScaledPosition(reference, scale, selectionQuad, h.Position);
}
private (bool X, bool Y) isQuadInBounds(Quad quad)
@@ -256,7 +229,7 @@ namespace osu.Game.Rulesets.Osu.Edit
{
var hitObjects = selectedMovableObjects;
Quad quad = getSurroundingQuad(hitObjects);
Quad quad = GeometryUtils.GetSurroundingQuad(hitObjects);
Vector2 delta = Vector2.Zero;
@@ -286,7 +259,7 @@ namespace osu.Game.Rulesets.Osu.Edit
float xOffset = ((reference & Anchor.x0) > 0) ? -scale.X : 0;
float yOffset = ((reference & Anchor.y0) > 0) ? -scale.Y : 0;
Quad selectionQuad = getSurroundingQuad(hitObjects);
Quad selectionQuad = GeometryUtils.GetSurroundingQuad(hitObjects);
//todo: this is not always correct for selections involving sliders. This approximation assumes each point is scaled independently, but sliderends move with the sliderhead.
Quad scaledQuad = new Quad(selectionQuad.TopLeft.X + xOffset, selectionQuad.TopLeft.Y + yOffset, selectionQuad.Width + scale.X, selectionQuad.Height + scale.Y);
@@ -311,26 +284,6 @@ namespace osu.Game.Rulesets.Osu.Edit
return scale;
}
/// <summary>
/// Returns a gamefield-space quad surrounding the provided hit objects.
/// </summary>
/// <param name="hitObjects">The hit objects to calculate a quad for.</param>
private Quad getSurroundingQuad(OsuHitObject[] hitObjects) =>
GetSurroundingQuad(hitObjects.SelectMany(h =>
{
if (h is IHasPath path)
{
return new[]
{
h.Position,
// can't use EndPosition for reverse slider cases.
h.Position + path.Path.PositionAt(1)
};
}
return new[] { h.Position };
}));
/// <summary>
/// All osu! hitobjects which can be moved/rotated/scaled.
/// </summary>
@@ -0,0 +1,107 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Utils;
using osuTK;
namespace osu.Game.Rulesets.Osu.Edit
{
public partial class OsuSelectionRotationHandler : SelectionRotationHandler
{
[Resolved]
private IEditorChangeHandler? changeHandler { get; set; }
private BindableList<HitObject> selectedItems { get; } = new BindableList<HitObject>();
[BackgroundDependencyLoader]
private void load(EditorBeatmap editorBeatmap)
{
selectedItems.BindTo(editorBeatmap.SelectedHitObjects);
}
protected override void LoadComplete()
{
base.LoadComplete();
selectedItems.CollectionChanged += (_, __) => updateState();
updateState();
}
private void updateState()
{
var quad = GeometryUtils.GetSurroundingQuad(selectedMovableObjects);
CanRotate.Value = quad.Width > 0 || quad.Height > 0;
}
private OsuHitObject[]? objectsInRotation;
private Vector2? defaultOrigin;
private Dictionary<OsuHitObject, Vector2>? originalPositions;
private Dictionary<IHasPath, Vector2[]>? originalPathControlPointPositions;
public override void Begin()
{
if (objectsInRotation != null)
throw new InvalidOperationException($"Cannot {nameof(Begin)} a rotate operation while another is in progress!");
changeHandler?.BeginChange();
objectsInRotation = selectedMovableObjects.ToArray();
defaultOrigin = GeometryUtils.GetSurroundingQuad(objectsInRotation).Centre;
originalPositions = objectsInRotation.ToDictionary(obj => obj, obj => obj.Position);
originalPathControlPointPositions = objectsInRotation.OfType<IHasPath>().ToDictionary(
obj => obj,
obj => obj.Path.ControlPoints.Select(point => point.Position).ToArray());
}
public override void Update(float rotation, Vector2? origin = null)
{
if (objectsInRotation == null)
throw new InvalidOperationException($"Cannot {nameof(Update)} a rotate operation without calling {nameof(Begin)} first!");
Debug.Assert(originalPositions != null && originalPathControlPointPositions != null && defaultOrigin != null);
Vector2 actualOrigin = origin ?? defaultOrigin.Value;
foreach (var ho in objectsInRotation)
{
ho.Position = GeometryUtils.RotatePointAroundOrigin(originalPositions[ho], actualOrigin, rotation);
if (ho is IHasPath withPath)
{
var originalPath = originalPathControlPointPositions[withPath];
for (int i = 0; i < withPath.Path.ControlPoints.Count; ++i)
withPath.Path.ControlPoints[i].Position = GeometryUtils.RotatePointAroundOrigin(originalPath[i], Vector2.Zero, rotation);
}
}
}
public override void Commit()
{
if (objectsInRotation == null)
throw new InvalidOperationException($"Cannot {nameof(Commit)} a rotate operation without calling {nameof(Begin)} first!");
changeHandler?.EndChange();
objectsInRotation = null;
originalPositions = null;
originalPathControlPointPositions = null;
defaultOrigin = null;
}
private IEnumerable<OsuHitObject> selectedMovableObjects => selectedItems.Cast<OsuHitObject>()
.Where(h => h is not Spinner);
}
}
@@ -72,9 +72,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
lastAngle = thisAngle;
IsSpinning.Value = isSpinnableTime && Math.Abs(currentRotation / 2 - Rotation) > 5f;
IsSpinning.Value = isSpinnableTime && Math.Abs(currentRotation - Rotation) > 10f;
Rotation = (float)Interpolation.Damp(Rotation, currentRotation / 2, 0.99, Math.Abs(Time.Elapsed));
Rotation = (float)Interpolation.Damp(Rotation, currentRotation, 0.99, Math.Abs(Time.Elapsed));
}
/// <summary>
+2 -2
View File
@@ -5,7 +5,7 @@
<key>CFBundleName</key>
<string>osu.Game.Rulesets.Taiko.Tests.iOS</string>
<key>CFBundleIdentifier</key>
<string>ppy.osu-Game-Rulesets-Taiko-Tests-iOS</string>
<string>sh.ppy.taiko-ruleset-tests</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
@@ -42,4 +42,4 @@
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
</dict>
</plist>
</plist>
@@ -36,6 +36,28 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements
AssertResult<Hit>(0, HitResult.Great);
}
[Test]
public void TestHitWithBothKeysOnSameFrameDoesNotFallThroughToNextObject()
{
PerformTest(new List<ReplayFrame>
{
new TaikoReplayFrame(0),
new TaikoReplayFrame(1000, TaikoAction.LeftCentre, TaikoAction.RightCentre),
}, CreateBeatmap(new Hit
{
Type = HitType.Centre,
StartTime = 1000,
}, new Hit
{
Type = HitType.Centre,
StartTime = 1020
}));
AssertJudgementCount(2);
AssertResult<Hit>(0, HitResult.Great);
AssertResult<Hit>(1, HitResult.Miss);
}
[Test]
public void TestHitRimHit()
{
@@ -1,24 +1,71 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Rulesets.Taiko.Tests.Mods
{
public partial class TestSceneTaikoModHidden : TaikoModTestScene
{
private Func<bool> checkAllMaxResultJudgements(int count) => ()
=> Player.ScoreProcessor.JudgedHits >= count
&& Player.Results.All(result => result.Type == result.Judgement.MaxResult);
[Test]
public void TestDefaultBeatmapTest() => CreateModTest(new ModTestData
{
Mod = new TaikoModHidden(),
Autoplay = true,
PassCondition = checkSomeAutoplayHits
PassCondition = checkAllMaxResultJudgements(4),
});
private bool checkSomeAutoplayHits()
=> Player.ScoreProcessor.JudgedHits >= 4
&& Player.Results.All(result => result.Type == result.Judgement.MaxResult);
[Test]
public void TestHitTwoNotesWithinShortPeriod()
{
const double hit_time = 1;
var beatmap = new Beatmap<TaikoHitObject>
{
HitObjects = new List<TaikoHitObject>
{
new Hit
{
Type = HitType.Rim,
StartTime = hit_time,
},
new Hit
{
Type = HitType.Centre,
StartTime = hit_time * 2,
},
},
BeatmapInfo =
{
Difficulty = new BeatmapDifficulty
{
SliderTickRate = 4,
OverallDifficulty = 0,
},
Ruleset = new TaikoRuleset().RulesetInfo
},
};
beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 0.1f });
CreateModTest(new ModTestData
{
Mod = new TaikoModHidden(),
Autoplay = true,
PassCondition = checkAllMaxResultJudgements(2),
Beatmap = beatmap,
});
}
}
}
@@ -11,6 +11,8 @@ namespace osu.Game.Rulesets.Taiko.Edit
{
public partial class TaikoHitObjectComposer : HitObjectComposer<TaikoHitObject>
{
protected override bool ApplyHorizontalCentering => false;
public TaikoHitObjectComposer(TaikoRuleset ruleset)
: base(ruleset)
{
@@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
private bool validActionPressed;
private bool pressHandledThisFrame;
private double? lastPressHandleTime;
private readonly Bindable<HitType> type = new Bindable<HitType>();
@@ -76,7 +76,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
HitActions = null;
HitAction = null;
validActionPressed = pressHandledThisFrame = false;
validActionPressed = false;
lastPressHandleTime = null;
}
private void updateActionsFromType()
@@ -114,7 +115,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
public override bool OnPressed(KeyBindingPressEvent<TaikoAction> e)
{
if (pressHandledThisFrame)
if (lastPressHandleTime == Time.Current)
return true;
if (Judged)
return false;
@@ -128,7 +129,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
// Regardless of whether we've hit or not, any secondary key presses in the same frame should be discarded
// E.g. hitting a non-strong centre as a strong should not fall through and perform a hit on the next note
pressHandledThisFrame = true;
lastPressHandleTime = Time.Current;
return result;
}
@@ -139,15 +140,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
base.OnReleased(e);
}
protected override void Update()
{
base.Update();
// The input manager processes all input prior to us updating, so this is the perfect time
// for us to remove the extra press blocking, before input is handled in the next frame
pressHandledThisFrame = false;
}
protected override void UpdateHitStateTransforms(ArmedState state)
{
Debug.Assert(HitObject.HitWindows != null);
+2 -2
View File
@@ -5,7 +5,7 @@
<key>CFBundleName</key>
<string>osu.Game.Tests.iOS</string>
<key>CFBundleIdentifier</key>
<string>ppy.osu-Game-Tests-iOS</string>
<string>sh.ppy.osu-tests</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
@@ -42,4 +42,4 @@
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
</dict>
</plist>
</plist>
@@ -0,0 +1,183 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Editing.Checks
{
public class CheckBreaksTest
{
private CheckBreaks check = null!;
[SetUp]
public void Setup()
{
check = new CheckBreaks();
}
[Test]
public void TestBreakTooShort()
{
var beatmap = new Beatmap<HitObject>
{
Breaks = new List<BreakPeriod>
{
new BreakPeriod(0, 649)
}
};
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckBreaks.IssueTemplateTooShort);
}
[Test]
public void TestBreakStartsEarly()
{
var beatmap = new Beatmap<HitObject>
{
HitObjects =
{
new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 1_200 }
},
Breaks = new List<BreakPeriod>
{
new BreakPeriod(100, 751)
}
};
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckBreaks.IssueTemplateEarlyStart);
}
[Test]
public void TestBreakEndsLate()
{
var beatmap = new Beatmap<HitObject>
{
HitObjects =
{
new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 1_298 }
},
Breaks = new List<BreakPeriod>
{
new BreakPeriod(200, 850)
}
};
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckBreaks.IssueTemplateLateEnd);
}
[Test]
public void TestBreakAfterLastObjectStartsEarly()
{
var beatmap = new Beatmap<HitObject>
{
HitObjects =
{
new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 1200 }
},
Breaks = new List<BreakPeriod>
{
new BreakPeriod(1398, 2300)
}
};
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckBreaks.IssueTemplateEarlyStart);
}
[Test]
public void TestBreakBeforeFirstObjectEndsLate()
{
var beatmap = new Beatmap<HitObject>
{
HitObjects =
{
new HitCircle { StartTime = 1100 },
new HitCircle { StartTime = 1500 }
},
Breaks = new List<BreakPeriod>
{
new BreakPeriod(0, 652)
}
};
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckBreaks.IssueTemplateLateEnd);
}
[Test]
public void TestBreakMultipleObjectsEarly()
{
var beatmap = new Beatmap<HitObject>
{
HitObjects =
{
new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 1_297 },
new HitCircle { StartTime = 1_298 }
},
Breaks = new List<BreakPeriod>
{
new BreakPeriod(200, 850)
}
};
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckBreaks.IssueTemplateLateEnd);
}
[Test]
public void TestBreaksCorrect()
{
var beatmap = new Beatmap<HitObject>
{
HitObjects =
{
new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 1_300 }
},
Breaks = new List<BreakPeriod>
{
new BreakPeriod(200, 850)
}
};
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
var issues = check.Run(context).ToList();
Assert.That(issues, Is.Empty);
}
}
}
@@ -14,14 +14,14 @@ using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Editing.Checks
{
public class CheckDrainTimeTest
public class CheckDrainLengthTest
{
private CheckDrainTime check = null!;
private CheckDrainLength check = null!;
[SetUp]
public void Setup()
{
check = new CheckDrainTime();
check = new CheckDrainLength();
}
[Test]
@@ -40,7 +40,7 @@ namespace osu.Game.Tests.Editing.Checks
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckDrainTime.IssueTemplateTooShort);
Assert.That(issues.Single().Template is CheckDrainLength.IssueTemplateTooShort);
}
[Test]
@@ -63,7 +63,7 @@ namespace osu.Game.Tests.Editing.Checks
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckDrainTime.IssueTemplateTooShort);
Assert.That(issues.Single().Template is CheckDrainLength.IssueTemplateTooShort);
}
[Test]
@@ -125,13 +125,13 @@ namespace osu.Game.Tests.Visual.Background
createFakeStoryboard();
AddStep("Enable Storyboard", () =>
{
player.ReplacesBackground.Value = true;
player.StoryboardReplacesBackground.Value = true;
player.StoryboardEnabled.Value = true;
});
AddUntilStep("Background is invisible, storyboard is visible", () => songSelect.IsBackgroundInvisible() && player.IsStoryboardVisible);
AddUntilStep("Background is black, storyboard is visible", () => songSelect.IsBackgroundVisible() && songSelect.IsBackgroundBlack() && player.IsStoryboardVisible);
AddStep("Disable Storyboard", () =>
{
player.ReplacesBackground.Value = false;
player.StoryboardReplacesBackground.Value = false;
player.StoryboardEnabled.Value = false;
});
AddUntilStep("Background is visible, storyboard is invisible", () => songSelect.IsBackgroundVisible() && !player.IsStoryboardVisible);
@@ -173,7 +173,7 @@ namespace osu.Game.Tests.Visual.Background
createFakeStoryboard();
AddStep("Enable Storyboard", () =>
{
player.ReplacesBackground.Value = true;
player.StoryboardReplacesBackground.Value = true;
player.StoryboardEnabled.Value = true;
});
AddStep("Enable user dim", () => player.DimmableStoryboard.IgnoreUserSettings.Value = false);
@@ -188,7 +188,7 @@ namespace osu.Game.Tests.Visual.Background
{
performFullSetup();
createFakeStoryboard();
AddStep("Enable replacing background", () => player.ReplacesBackground.Value = true);
AddStep("Enable replacing background", () => player.StoryboardReplacesBackground.Value = true);
AddUntilStep("Storyboard is invisible", () => !player.IsStoryboardVisible);
AddUntilStep("Background is visible", () => songSelect.IsBackgroundVisible());
@@ -199,11 +199,11 @@ namespace osu.Game.Tests.Visual.Background
player.DimmableStoryboard.IgnoreUserSettings.Value = true;
});
AddUntilStep("Storyboard is visible", () => player.IsStoryboardVisible);
AddUntilStep("Background is invisible", () => songSelect.IsBackgroundInvisible());
AddUntilStep("Background is dimmed", () => songSelect.IsBackgroundVisible() && songSelect.IsBackgroundBlack());
AddStep("Disable background replacement", () => player.ReplacesBackground.Value = false);
AddStep("Disable background replacement", () => player.StoryboardReplacesBackground.Value = false);
AddUntilStep("Storyboard is visible", () => player.IsStoryboardVisible);
AddUntilStep("Background is visible", () => songSelect.IsBackgroundVisible());
AddUntilStep("Background is visible", () => songSelect.IsBackgroundVisible() && !songSelect.IsBackgroundBlack());
}
/// <summary>
@@ -257,7 +257,7 @@ namespace osu.Game.Tests.Visual.Background
private void createFakeStoryboard() => AddStep("Create storyboard", () =>
{
player.StoryboardEnabled.Value = false;
player.ReplacesBackground.Value = false;
player.StoryboardReplacesBackground.Value = false;
player.DimmableStoryboard.Add(new OsuSpriteText
{
Size = new Vector2(500, 50),
@@ -323,6 +323,8 @@ namespace osu.Game.Tests.Visual.Background
config.BindWith(OsuSetting.BlurLevel, BlurLevel);
}
public bool IsBackgroundBlack() => background.CurrentColour == OsuColour.Gray(0);
public bool IsBackgroundDimmed() => background.CurrentColour == OsuColour.Gray(1f - background.CurrentDim);
public bool IsBackgroundUndimmed() => background.CurrentColour == Color4.White;
@@ -331,8 +333,6 @@ namespace osu.Game.Tests.Visual.Background
public bool IsUserBlurDisabled() => background.CurrentBlur == new Vector2(0);
public bool IsBackgroundInvisible() => background.CurrentAlpha == 0;
public bool IsBackgroundVisible() => background.CurrentAlpha == 1;
public bool IsBackgroundBlur() => Precision.AlmostEquals(background.CurrentBlur, new Vector2(BACKGROUND_BLUR), 0.1f);
@@ -367,7 +367,7 @@ namespace osu.Game.Tests.Visual.Background
{
base.OnEntering(e);
ApplyToBackground(b => ReplacesBackground.BindTo(b.StoryboardReplacesBackground));
ApplyToBackground(b => StoryboardReplacesBackground.BindTo(b.StoryboardReplacesBackground));
}
public new DimmableStoryboard DimmableStoryboard => base.DimmableStoryboard;
@@ -376,7 +376,7 @@ namespace osu.Game.Tests.Visual.Background
public bool BlockLoad;
public Bindable<bool> StoryboardEnabled;
public readonly Bindable<bool> ReplacesBackground = new Bindable<bool>();
public readonly Bindable<bool> StoryboardReplacesBackground = new Bindable<bool>();
public readonly Bindable<bool> IsPaused = new Bindable<bool>();
public LoadBlockingTestPlayer(bool allowPause = true)
@@ -22,7 +22,6 @@ namespace osu.Game.Tests.Visual.Editing
public partial class TestSceneBeatDivisorControl : OsuManualInputManagerTestScene
{
private BeatDivisorControl beatDivisorControl = null!;
private BindableBeatDivisor bindableBeatDivisor = null!;
private SliderBar<int> tickSliderBar => beatDivisorControl.ChildrenOfType<SliderBar<int>>().Single();
private Triangle tickMarkerHead => tickSliderBar.ChildrenOfType<Triangle>().Single();
@@ -30,13 +29,19 @@ namespace osu.Game.Tests.Visual.Editing
[Cached]
private readonly OverlayColourProvider overlayColour = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
[Cached]
private readonly BindableBeatDivisor bindableBeatDivisor = new BindableBeatDivisor(16);
[SetUp]
public void SetUp() => Schedule(() =>
{
bindableBeatDivisor.ValidDivisors.SetDefault();
bindableBeatDivisor.SetDefault();
Child = new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
Child = beatDivisorControl = new BeatDivisorControl(bindableBeatDivisor = new BindableBeatDivisor(16))
Child = beatDivisorControl = new BeatDivisorControl
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -3,8 +3,11 @@
#nullable disable
using System;
using System.Linq;
using JetBrains.Annotations;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
@@ -20,6 +23,14 @@ namespace osu.Game.Tests.Visual.Editing
private Container selectionArea;
private SelectionBox selectionBox;
[Cached(typeof(SelectionRotationHandler))]
private TestSelectionRotationHandler rotationHandler;
public TestSceneComposeSelectBox()
{
rotationHandler = new TestSelectionRotationHandler(() => selectionArea);
}
[SetUp]
public void SetUp() => Schedule(() =>
{
@@ -34,13 +45,11 @@ namespace osu.Game.Tests.Visual.Editing
{
RelativeSizeAxes = Axes.Both,
CanRotate = true,
CanScaleX = true,
CanScaleY = true,
CanFlipX = true,
CanFlipY = true,
OnRotation = handleRotation,
OnScale = handleScale
}
}
@@ -71,11 +80,48 @@ namespace osu.Game.Tests.Visual.Editing
return true;
}
private bool handleRotation(float angle)
private partial class TestSelectionRotationHandler : SelectionRotationHandler
{
// kinda silly and wrong, but just showing that the drag handles work.
selectionArea.Rotation += angle;
return true;
private readonly Func<Container> getTargetContainer;
public TestSelectionRotationHandler(Func<Container> getTargetContainer)
{
this.getTargetContainer = getTargetContainer;
CanRotate.Value = true;
}
[CanBeNull]
private Container targetContainer;
private float? initialRotation;
public override void Begin()
{
if (targetContainer != null)
throw new InvalidOperationException($"Cannot {nameof(Begin)} a rotate operation while another is in progress!");
targetContainer = getTargetContainer();
initialRotation = targetContainer!.Rotation;
}
public override void Update(float rotation, Vector2? origin = null)
{
if (targetContainer == null)
throw new InvalidOperationException($"Cannot {nameof(Update)} a rotate operation without calling {nameof(Begin)} first!");
// kinda silly and wrong, but just showing that the drag handles work.
targetContainer.Rotation = initialRotation!.Value + rotation;
}
public override void Commit()
{
if (targetContainer == null)
throw new InvalidOperationException($"Cannot {nameof(Commit)} a rotate operation without calling {nameof(Begin)} first!");
targetContainer = null;
initialRotation = null;
}
}
[Test]
@@ -1,26 +1,24 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Tests.Beatmaps;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Tests.Beatmaps;
using osuTK;
using osuTK.Input;
@@ -82,7 +80,7 @@ namespace osu.Game.Tests.Visual.Editing
[Test]
public void TestNudgeSelection()
{
HitCircle[] addedObjects = null;
HitCircle[] addedObjects = null!;
AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects = new[]
{
@@ -104,7 +102,7 @@ namespace osu.Game.Tests.Visual.Editing
[Test]
public void TestRotateHotkeys()
{
HitCircle[] addedObjects = null;
HitCircle[] addedObjects = null!;
AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects = new[]
{
@@ -136,7 +134,7 @@ namespace osu.Game.Tests.Visual.Editing
[Test]
public void TestGlobalFlipHotkeys()
{
HitCircle addedObject = null;
HitCircle addedObject = null!;
AddStep("add hitobjects", () => EditorBeatmap.Add(addedObject = new HitCircle { StartTime = 100 }));
@@ -217,11 +215,173 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 2 && !EditorBeatmap.SelectedHitObjects.Contains(addedObjects[1]));
}
[Test]
public void TestNearestSelection()
{
var firstObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 0 };
var secondObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 600 };
AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[] { firstObject, secondObject }));
moveMouseToObject(() => firstObject);
AddStep("seek near first", () => EditorClock.Seek(100));
AddStep("left click", () => InputManager.Click(MouseButton.Left));
AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject));
AddStep("deselect", () => EditorBeatmap.SelectedHitObjects.Clear());
AddStep("seek near second", () => EditorClock.Seek(500));
AddStep("left click", () => InputManager.Click(MouseButton.Left));
AddAssert("second selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(secondObject));
AddStep("deselect", () => EditorBeatmap.SelectedHitObjects.Clear());
AddStep("seek halfway", () => EditorClock.Seek(300));
AddStep("left click", () => InputManager.Click(MouseButton.Left));
AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject));
}
[Test]
public void TestNearestSelectionWithEndTime()
{
var firstObject = new Slider
{
Position = new Vector2(256, 192),
StartTime = 0,
Path = new SliderPath(new[]
{
new PathControlPoint(),
new PathControlPoint(new Vector2(50, 0)),
})
};
var secondObject = new HitCircle
{
Position = new Vector2(256, 192),
StartTime = 600
};
AddStep("add hitobjects", () => EditorBeatmap.AddRange(new HitObject[] { firstObject, secondObject }));
moveMouseToObject(() => firstObject);
AddStep("seek near first", () => EditorClock.Seek(100));
AddStep("left click", () => InputManager.Click(MouseButton.Left));
AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject));
AddStep("deselect", () => EditorBeatmap.SelectedHitObjects.Clear());
AddStep("seek near second", () => EditorClock.Seek(500));
AddStep("left click", () => InputManager.Click(MouseButton.Left));
AddAssert("second selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(secondObject));
AddStep("deselect", () => EditorBeatmap.SelectedHitObjects.Clear());
AddStep("seek roughly halfway", () => EditorClock.Seek(350));
AddStep("left click", () => InputManager.Click(MouseButton.Left));
// Slider gets priority due to end time.
AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject));
}
[Test]
public void TestCyclicSelection()
{
var firstObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 0 };
var secondObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 300 };
var thirdObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 600 };
AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[] { firstObject, secondObject, thirdObject }));
moveMouseToObject(() => firstObject);
AddStep("left click", () => InputManager.Click(MouseButton.Left));
AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject));
AddStep("left click", () => InputManager.Click(MouseButton.Left));
AddAssert("second selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(secondObject));
AddStep("left click", () => InputManager.Click(MouseButton.Left));
AddAssert("third selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(thirdObject));
// cycle around
AddStep("left click", () => InputManager.Click(MouseButton.Left));
AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject));
}
[Test]
public void TestCyclicSelectionOutwards()
{
var firstObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 0 };
var secondObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 300 };
var thirdObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 600 };
AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[] { firstObject, secondObject, thirdObject }));
moveMouseToObject(() => firstObject);
AddStep("seek near second", () => EditorClock.Seek(320));
AddStep("left click", () => InputManager.Click(MouseButton.Left));
AddAssert("second selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(secondObject));
AddStep("left click", () => InputManager.Click(MouseButton.Left));
AddAssert("third selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(thirdObject));
AddStep("left click", () => InputManager.Click(MouseButton.Left));
AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject));
// cycle around
AddStep("left click", () => InputManager.Click(MouseButton.Left));
AddAssert("second selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(secondObject));
}
[Test]
public void TestCyclicSelectionBackwards()
{
var firstObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 0 };
var secondObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 200 };
var thirdObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 400 };
AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[] { firstObject, secondObject, thirdObject }));
moveMouseToObject(() => firstObject);
AddStep("seek to third", () => EditorClock.Seek(350));
AddStep("left click", () => InputManager.Click(MouseButton.Left));
AddAssert("third selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(thirdObject));
AddStep("left click", () => InputManager.Click(MouseButton.Left));
AddAssert("second selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(secondObject));
AddStep("left click", () => InputManager.Click(MouseButton.Left));
AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject));
// cycle around
AddStep("left click", () => InputManager.Click(MouseButton.Left));
AddAssert("third selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(thirdObject));
}
[Test]
public void TestDoubleClickToSeek()
{
var hitCircle = new HitCircle { Position = new Vector2(256, 192), StartTime = 600 };
AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[] { hitCircle }));
moveMouseToObject(() => hitCircle);
AddRepeatStep("double click", () => InputManager.Click(MouseButton.Left), 2);
AddUntilStep("seeked to circle", () => EditorClock.CurrentTime, () => Is.EqualTo(600));
}
[TestCase(false)]
[TestCase(true)]
public void TestMultiSelectFromDrag(bool alreadySelectedBeforeDrag)
{
HitCircle[] addedObjects = null;
HitCircle[] addedObjects = null!;
AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects = new[]
{
@@ -320,7 +480,7 @@ namespace osu.Game.Tests.Visual.Editing
[Test]
public void TestQuickDeleteRemovesSliderControlPoint()
{
Slider slider = null;
Slider slider = null!;
PathControlPoint[] points =
{
@@ -18,6 +18,7 @@ using osu.Game.Database;
using osu.Game.Overlays.Dialog;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
@@ -453,6 +454,51 @@ namespace osu.Game.Tests.Visual.Editing
});
}
[Test]
public void TestExitBlockedWhenSavingBeatmapWithSameNamedDifficulties()
{
Guid setId = Guid.Empty;
const string duplicate_difficulty_name = "duplicate";
AddStep("retrieve set ID", () => setId = EditorBeatmap.BeatmapInfo.BeatmapSet!.ID);
AddStep("set difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = duplicate_difficulty_name);
AddStep("save beatmap", () => Editor.Save());
AddAssert("new beatmap persisted", () =>
{
var set = beatmapManager.QueryBeatmapSet(s => s.ID == setId);
return set != null && set.PerformRead(s => s.Beatmaps.Count == 1 && s.Files.Count == 1);
});
AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new CatchRuleset().RulesetInfo));
AddUntilStep("wait for created", () =>
{
string? difficultyName = Editor.ChildrenOfType<EditorBeatmap>().SingleOrDefault()?.BeatmapInfo.DifficultyName;
return difficultyName != null && difficultyName != duplicate_difficulty_name;
});
AddUntilStep("wait for editor load", () => Editor.IsLoaded && DialogOverlay.IsLoaded);
AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[]
{
new Fruit
{
StartTime = 0
},
new Fruit
{
StartTime = 1000
}
}));
AddStep("set difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = duplicate_difficulty_name);
AddUntilStep("wait for has unsaved changes", () => Editor.HasUnsavedChanges);
AddStep("exit", () => Editor.Exit());
AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is PromptForSaveDialog);
AddStep("attempt to save", () => DialogOverlay.CurrentDialog.PerformOkAction());
AddAssert("editor is still current", () => Editor.IsCurrentScreen());
}
[Test]
public void TestCreateNewDifficultyForInconvertibleRuleset()
{
@@ -117,9 +117,9 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("move mouse to overlapping toggle button", () =>
{
var playfield = hitObjectComposer.Playfield.ScreenSpaceDrawQuad;
var button = toolboxContainer.ChildrenOfType<DrawableTernaryButton>().First(b => playfield.Contains(b.ScreenSpaceDrawQuad.Centre));
var button = toolboxContainer.ChildrenOfType<DrawableTernaryButton>().First(b => playfield.Contains(getOverlapPoint(b)));
InputManager.MoveMouseTo(button);
InputManager.MoveMouseTo(getOverlapPoint(button));
});
AddAssert("no circles placed", () => editorBeatmap.HitObjects.Count == 0);
@@ -127,6 +127,12 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("attempt place circle", () => InputManager.Click(MouseButton.Left));
AddAssert("no circles placed", () => editorBeatmap.HitObjects.Count == 0);
Vector2 getOverlapPoint(DrawableTernaryButton ternaryButton)
{
var quad = ternaryButton.ScreenSpaceDrawQuad;
return quad.TopLeft + new Vector2(quad.Width * 9 / 10, quad.Height / 2);
}
}
[Test]
@@ -23,7 +23,7 @@ namespace osu.Game.Tests.Visual.Editing
{
BeatDivisor.Value = 4;
Add(new BeatDivisorControl(BeatDivisor)
Add(new BeatDivisorControl
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
@@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private HUDOverlay hudOverlay = null!;
[Cached(typeof(ScoreProcessor))]
private ScoreProcessor scoreProcessor => gameplayState.ScoreProcessor;
private ScoreProcessor scoreProcessor { get; set; }
[Cached(typeof(HealthProcessor))]
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
@@ -47,6 +47,11 @@ namespace osu.Game.Tests.Visual.Gameplay
private Drawable hideTarget => hudOverlay.ChildrenOfType<SkinComponentsContainer>().First();
private Drawable keyCounterFlow => hudOverlay.ChildrenOfType<KeyCounterDisplay>().First().ChildrenOfType<FillFlowContainer<KeyCounter>>().Single();
public TestSceneHUDOverlay()
{
scoreProcessor = gameplayState.ScoreProcessor;
}
[BackgroundDependencyLoader]
private void load()
{
@@ -69,13 +69,13 @@ namespace osu.Game.Tests.Visual.Gameplay
spewer.Clock = new FramedClock(testClock);
});
AddStep("start spewer", () => spewer.Active.Value = true);
AddAssert("spawned first particle", () => spewer.TotalCreatedParticles == 1);
AddAssert("spawned first particle", () => spewer.TotalCreatedParticles, () => Is.EqualTo(1));
AddStep("move clock forward", () => testClock.CurrentTime = TestParticleSpewer.MAX_DURATION * 3);
AddAssert("spawned second particle", () => spewer.TotalCreatedParticles == 2);
AddAssert("spawned second particle", () => spewer.TotalCreatedParticles, () => Is.EqualTo(2));
AddStep("move clock backwards", () => testClock.CurrentTime = TestParticleSpewer.MAX_DURATION * -1);
AddAssert("spawned third particle", () => spewer.TotalCreatedParticles == 3);
AddAssert("spawned third particle", () => spewer.TotalCreatedParticles, () => Is.EqualTo(3));
}
private TestParticleSpewer createSpewer() =>
@@ -21,9 +21,11 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Tests.Resources;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
{
@@ -147,6 +149,38 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("score in database", () => Realm.Run(r => r.Find<ScoreInfo>(Player.Score.ScoreInfo.ID) != null));
}
[Test]
public void TestReplayExport()
{
CreateTest();
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime()));
AddUntilStep("results displayed", () => (Player.GetChildScreen() as ResultsScreen)?.IsLoaded == true);
AddUntilStep("score in database", () => Realm.Run(r => r.Find<ScoreInfo>(Player.Score.ScoreInfo.ID) != null));
AddUntilStep("wait for button clickable", () => ((OsuScreen)Player.GetChildScreen())
.ChildrenOfType<ReplayDownloadButton>().FirstOrDefault()?
.ChildrenOfType<OsuClickableContainer>().FirstOrDefault()?
.Enabled.Value == true);
AddAssert("no export files", () => !LocalStorage.GetFiles("exports").Any());
AddStep("Export replay", () => InputManager.PressKey(Key.F2));
string? filePath = null;
// Files starting with _ are temporary, created by CreateFileSafely call.
AddUntilStep("wait for export file", () => filePath = LocalStorage.GetFiles("exports").SingleOrDefault(f => !f.StartsWith("_", StringComparison.Ordinal)), () => Is.Not.Null);
AddAssert("filesize is non-zero", () =>
{
using (var stream = LocalStorage.GetStream(filePath))
return stream.Length;
}, () => Is.Not.Zero);
}
[Test]
public void TestScoreStoredLocallyCustomRuleset()
{
@@ -138,24 +138,28 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestCyclicSelection()
{
SkinBlueprint[] blueprints = null!;
List<SkinBlueprint> blueprints = new List<SkinBlueprint>();
AddStep("Add big black boxes", () =>
AddStep("clear list", () => blueprints.Clear());
for (int i = 0; i < 3; i++)
{
InputManager.MoveMouseTo(skinEditor.ChildrenOfType<BigBlackBox>().First());
InputManager.Click(MouseButton.Left);
InputManager.Click(MouseButton.Left);
InputManager.Click(MouseButton.Left);
});
AddStep("Add big black box", () =>
{
InputManager.MoveMouseTo(skinEditor.ChildrenOfType<BigBlackBox>().First());
InputManager.Click(MouseButton.Left);
});
AddStep("store box", () =>
{
// Add blueprints one-by-one so we have a stable order for testing reverse cyclic selection against.
blueprints.Add(skinEditor.ChildrenOfType<SkinBlueprint>().Single(s => s.IsSelected));
});
}
AddAssert("Three black boxes added", () => targetContainer.Components.OfType<BigBlackBox>().Count(), () => Is.EqualTo(3));
AddStep("Store black box blueprints", () =>
{
blueprints = skinEditor.ChildrenOfType<SkinBlueprint>().Where(b => b.Item is BigBlackBox).ToArray();
});
AddAssert("Selection is black box 1", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[0].Item));
AddAssert("Selection is last", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[2].Item));
AddStep("move cursor to black box", () =>
{
@@ -164,13 +168,13 @@ namespace osu.Game.Tests.Visual.Gameplay
});
AddStep("click on black box stack", () => InputManager.Click(MouseButton.Left));
AddAssert("Selection is black box 2", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[1].Item));
AddAssert("Selection is second last", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[1].Item));
AddStep("click on black box stack", () => InputManager.Click(MouseButton.Left));
AddAssert("Selection is black box 3", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[2].Item));
AddAssert("Selection is last", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[0].Item));
AddStep("click on black box stack", () => InputManager.Click(MouseButton.Left));
AddAssert("Selection is black box 1", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[0].Item));
AddAssert("Selection is first", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[2].Item));
AddStep("select all boxes", () =>
{
@@ -8,6 +8,7 @@ using osu.Game.Overlays;
using osu.Game.Overlays.SkinEditor;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Skinning;
namespace osu.Game.Tests.Visual.Gameplay
{
@@ -19,7 +20,9 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestToggleEditor()
{
AddStep("show available components", () => SetContents(_ => new SkinComponentToolbox
var skinComponentsContainer = new SkinComponentsContainer(new SkinComponentsContainerLookup(SkinComponentsContainerLookup.TargetArea.SongSelect));
AddStep("show available components", () => SetContents(_ => new SkinComponentToolbox(skinComponentsContainer, null)
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
@@ -23,7 +23,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public partial class TestSceneSkinEditorMultipleSkins : SkinnableTestScene
{
[Cached(typeof(ScoreProcessor))]
private ScoreProcessor scoreProcessor => gameplayState.ScoreProcessor;
private ScoreProcessor scoreProcessor { get; set; }
[Cached(typeof(HealthProcessor))]
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
@@ -37,6 +37,11 @@ namespace osu.Game.Tests.Visual.Gameplay
[Cached]
public readonly EditorClipboard Clipboard = new EditorClipboard();
public TestSceneSkinEditorMultipleSkins()
{
scoreProcessor = gameplayState.ScoreProcessor;
}
[SetUpSteps]
public void SetUpSteps()
{
@@ -30,7 +30,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private HUDOverlay hudOverlay;
[Cached(typeof(ScoreProcessor))]
private ScoreProcessor scoreProcessor => gameplayState.ScoreProcessor;
private ScoreProcessor scoreProcessor { get; set; }
[Cached(typeof(HealthProcessor))]
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
@@ -47,6 +47,11 @@ namespace osu.Game.Tests.Visual.Gameplay
private Drawable hideTarget => hudOverlay.ChildrenOfType<SkinComponentsContainer>().First();
private Drawable keyCounterFlow => hudOverlay.ChildrenOfType<KeyCounterDisplay>().First().ChildrenOfType<FillFlowContainer<KeyCounter>>().Single();
public TestSceneSkinnableHUDOverlay()
{
scoreProcessor = gameplayState.ScoreProcessor;
}
[Test]
public void TestComboCounterIncrementing()
{
@@ -20,7 +20,6 @@ namespace osu.Game.Tests.Visual.Gameplay
{
CreateModTest(new ModTestData
{
Beatmap = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo).Beatmap,
Mod = new UnknownMod("WNG"),
PassCondition = () => Player.IsLoaded && !Player.LoadedBeatmapSuccessfully
});
@@ -0,0 +1,52 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Screens.Menu;
namespace osu.Game.Tests.Visual.Menus
{
[TestFixture]
public partial class TestSceneStarFountain : OsuTestScene
{
[SetUpSteps]
public void SetUpSteps()
{
AddStep("make fountains", () =>
{
Children = new[]
{
new StarFountain
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
X = 200,
},
new StarFountain
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
X = -200,
},
};
});
}
[Test]
public void TestPew()
{
AddRepeatStep("activate fountains sometimes", () =>
{
foreach (var fountain in Children.OfType<StarFountain>())
{
if (RNG.NextSingle() > 0.8f)
fountain.Shoot();
}
}, 150);
}
}
}
@@ -65,6 +65,34 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("clear playing users", () => playingUsers.Clear());
}
[TestCase(1)]
[TestCase(4)]
[TestCase(9)]
public void TestGeneral(int count)
{
int[] userIds = getPlayerIds(count);
start(userIds);
loadSpectateScreen();
sendFrames(userIds, 1000);
AddWaitStep("wait a bit", 20);
}
[Test]
public void TestMultipleStartRequests()
{
int[] userIds = getPlayerIds(2);
start(userIds);
loadSpectateScreen();
sendFrames(userIds, 1000);
AddWaitStep("wait a bit", 20);
start(userIds);
}
[Test]
public void TestDelayedStart()
{
@@ -88,18 +116,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("two players added", () => spectatorScreen.ChildrenOfType<Player>().Count() == 2);
}
[Test]
public void TestGeneral()
{
int[] userIds = getPlayerIds(4);
start(userIds);
loadSpectateScreen();
sendFrames(userIds, 1000);
AddWaitStep("wait a bit", 20);
}
[Test]
public void TestSpectatorPlayerInteractiveElementsHidden()
{
@@ -67,6 +67,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for present", () => songSelect.IsCurrentScreen() && songSelect.BeatmapSetsLoaded);
}
[Test]
public void TestSelectFreeMods()
{
AddStep("set some freemods", () => songSelect.FreeMods.Value = new OsuRuleset().GetModsFor(ModType.Fun).ToArray());
AddStep("set all freemods", () => songSelect.FreeMods.Value = new OsuRuleset().CreateAllMods().ToArray());
AddStep("set no freemods", () => songSelect.FreeMods.Value = Array.Empty<Mod>());
}
[Test]
public void TestBeatmapConfirmed()
{
@@ -170,6 +170,39 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("time is correct", () => getEditor().ChildrenOfType<EditorClock>().First().CurrentTime, () => Is.EqualTo(1234));
}
[Test]
public void TestAttemptGlobalMusicOperationFromEditor()
{
BeatmapSetInfo beatmapSet = null!;
AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely());
AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach());
AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet));
AddUntilStep("wait for song select",
() => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
&& Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
&& songSelect.IsLoaded);
AddUntilStep("wait for music playing", () => Game.MusicController.IsPlaying);
AddStep("user request stop", () => Game.MusicController.Stop(requestedByUser: true));
AddUntilStep("wait for music stopped", () => !Game.MusicController.IsPlaying);
AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0)));
AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
AddUntilStep("music still stopped", () => !Game.MusicController.IsPlaying);
AddStep("user request play", () => Game.MusicController.Play(requestedByUser: true));
AddUntilStep("music still stopped", () => !Game.MusicController.IsPlaying);
AddStep("exit to song select", () => Game.PerformFromScreen(_ => { }, typeof(PlaySongSelect).Yield()));
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect);
AddUntilStep("wait for music playing", () => Game.MusicController.IsPlaying);
AddStep("user request stop", () => Game.MusicController.Stop(requestedByUser: true));
AddUntilStep("wait for music stopped", () => !Game.MusicController.IsPlaying);
}
private EditorBeatmap getEditorBeatmap() => getEditor().ChildrenOfType<EditorBeatmap>().Single();
private Editor getEditor() => (Editor)Game.ScreenStack.CurrentScreen;
@@ -13,6 +13,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Overlays;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Play;
@@ -86,6 +87,29 @@ namespace osu.Game.Tests.Visual.Navigation
AddAssert("did perform", () => actionPerformed);
}
[Test]
public void TestPerformAtMenuFromPlayerLoaderWithAutoplayShortcut()
{
importAndWaitForSongSelect();
AddStep("press ctrl+enter", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.Enter);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddUntilStep("Wait for new screen", () => Game.ScreenStack.CurrentScreen is PlayerLoader);
AddAssert("Mods include autoplay", () => Game.SelectedMods.Value.Any(m => m is ModAutoplay));
AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true));
AddUntilStep("returned to main menu", () => Game.ScreenStack.CurrentScreen is MainMenu);
AddAssert("did perform", () => actionPerformed);
AddAssert("Mods don't include autoplay", () => !Game.SelectedMods.Value.Any(m => m is ModAutoplay));
}
[Test]
public void TestPerformEnsuresScreenIsLoaded()
{
@@ -722,6 +722,30 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("Wait for game exit", () => Game.ScreenStack.CurrentScreen == null);
}
[Test]
public void TestForceExitWithOperationInProgress()
{
AddStep("set hold delay to 0", () => Game.LocalConfig.SetValue(OsuSetting.UIHoldActivationDelay, 0.0));
AddUntilStep("wait for dialog overlay", () => Game.ChildrenOfType<DialogOverlay>().SingleOrDefault() != null);
AddStep("start ongoing operation", () =>
{
Game.Notifications.Post(new ProgressNotification
{
Text = "Something is still running",
Progress = 0.5f,
State = ProgressNotificationState.Active,
});
});
AddStep("attempt exit", () =>
{
for (int i = 0; i < 2; ++i)
Game.ScreenStack.CurrentScreen.Exit();
});
AddUntilStep("stopped at exit confirm", () => Game.ChildrenOfType<DialogOverlay>().Single().CurrentDialog is ConfirmExitDialog);
}
[Test]
public void TestExitGameFromSongSelect()
{
@@ -154,7 +154,14 @@ namespace osu.Game.Tests.Visual.Online
Type = ChangelogEntryType.Misc,
Category = "Code quality",
Title = "Clean up another thing"
}
},
new APIChangelogEntry
{
Type = ChangelogEntryType.Add,
Category = "osu!",
Title = "Add entry with news url",
Url = "https://osu.ppy.sh/home/news/2023-07-27-summer-splash"
},
}
});
@@ -56,38 +56,38 @@ namespace osu.Game.Tests.Visual
public void AllowTrackAdjustmentsTest()
{
AddStep("push allowing screen", () => stack.Push(loadNewScreen<AllowScreen>()));
AddAssert("allows adjustments 1", () => musicController.AllowTrackAdjustments);
AddAssert("allows adjustments 1", () => musicController.ApplyModTrackAdjustments);
AddStep("push inheriting screen", () => stack.Push(loadNewScreen<InheritScreen>()));
AddAssert("allows adjustments 2", () => musicController.AllowTrackAdjustments);
AddAssert("allows adjustments 2", () => musicController.ApplyModTrackAdjustments);
AddStep("push disallowing screen", () => stack.Push(loadNewScreen<DisallowScreen>()));
AddAssert("disallows adjustments 3", () => !musicController.AllowTrackAdjustments);
AddAssert("disallows adjustments 3", () => !musicController.ApplyModTrackAdjustments);
AddStep("push inheriting screen", () => stack.Push(loadNewScreen<InheritScreen>()));
AddAssert("disallows adjustments 4", () => !musicController.AllowTrackAdjustments);
AddAssert("disallows adjustments 4", () => !musicController.ApplyModTrackAdjustments);
AddStep("push inheriting screen", () => stack.Push(loadNewScreen<InheritScreen>()));
AddAssert("disallows adjustments 5", () => !musicController.AllowTrackAdjustments);
AddAssert("disallows adjustments 5", () => !musicController.ApplyModTrackAdjustments);
AddStep("push allowing screen", () => stack.Push(loadNewScreen<AllowScreen>()));
AddAssert("allows adjustments 6", () => musicController.AllowTrackAdjustments);
AddAssert("allows adjustments 6", () => musicController.ApplyModTrackAdjustments);
// Now start exiting from screens
AddStep("exit screen", () => stack.Exit());
AddAssert("disallows adjustments 7", () => !musicController.AllowTrackAdjustments);
AddAssert("disallows adjustments 7", () => !musicController.ApplyModTrackAdjustments);
AddStep("exit screen", () => stack.Exit());
AddAssert("disallows adjustments 8", () => !musicController.AllowTrackAdjustments);
AddAssert("disallows adjustments 8", () => !musicController.ApplyModTrackAdjustments);
AddStep("exit screen", () => stack.Exit());
AddAssert("disallows adjustments 9", () => !musicController.AllowTrackAdjustments);
AddAssert("disallows adjustments 9", () => !musicController.ApplyModTrackAdjustments);
AddStep("exit screen", () => stack.Exit());
AddAssert("allows adjustments 10", () => musicController.AllowTrackAdjustments);
AddAssert("allows adjustments 10", () => musicController.ApplyModTrackAdjustments);
AddStep("exit screen", () => stack.Exit());
AddAssert("allows adjustments 11", () => musicController.AllowTrackAdjustments);
AddAssert("allows adjustments 11", () => musicController.ApplyModTrackAdjustments);
}
public partial class TestScreen : ScreenWithBeatmapBackground
@@ -129,12 +129,12 @@ namespace osu.Game.Tests.Visual
private partial class AllowScreen : OsuScreen
{
public override bool? AllowTrackAdjustments => true;
public override bool? ApplyModTrackAdjustments => true;
}
public partial class DisallowScreen : OsuScreen
{
public override bool? AllowTrackAdjustments => false;
public override bool? ApplyModTrackAdjustments => false;
}
private partial class InheritScreen : OsuScreen
@@ -5,7 +5,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Audio.Track;
@@ -283,8 +282,6 @@ namespace osu.Game.Tests.Visual.UserInterface
if (ReferenceEquals(timingPoints[^1], current))
{
Debug.Assert(BeatSyncSource.Clock != null);
return (int)Math.Ceiling((BeatSyncSource.Clock.CurrentTime - current.Time) / current.BeatLength);
}
@@ -295,8 +292,6 @@ namespace osu.Game.Tests.Visual.UserInterface
{
base.Update();
Debug.Assert(BeatSyncSource.Clock != null);
timeUntilNextBeat.Value = TimeUntilNextBeat;
timeSinceLastBeat.Value = TimeSinceLastBeat;
currentTime.Value = BeatSyncSource.Clock.CurrentTime;
@@ -542,7 +542,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("clear search", () => modSelectOverlay.SearchTerm = string.Empty);
AddStep("press enter", () => InputManager.Key(Key.Enter));
AddAssert("mod select still visible", () => modSelectOverlay.State.Value == Visibility.Visible);
AddAssert("mod select hidden", () => modSelectOverlay.State.Value == Visibility.Hidden);
}
[Test]
@@ -99,16 +99,18 @@ namespace osu.Game.Tests.Visual.UserInterface
{
TestUpdateableOnlineBeatmapSetCover updateableCover = null;
AddStep("setup cover", () => Child = updateableCover = new TestUpdateableOnlineBeatmapSetCover
AddStep("setup cover", () => Child = updateableCover = new TestUpdateableOnlineBeatmapSetCover(400)
{
OnlineInfo = CreateAPIBeatmapSet(),
RelativeSizeAxes = Axes.Both,
Masking = true,
});
AddStep("change model", () => updateableCover.OnlineInfo = null);
AddWaitStep("wait some", 5);
AddAssert("no cover added", () => !updateableCover.ChildrenOfType<DelayedLoadUnloadWrapper>().Any());
AddStep("change model to null", () => updateableCover.OnlineInfo = null);
AddUntilStep("wait for load", () => updateableCover.DelayedLoadFinished);
AddAssert("no cover added", () => !updateableCover.ChildrenOfType<TestOnlineBeatmapSetCover>().Any());
}
[Test]
@@ -143,11 +145,19 @@ namespace osu.Game.Tests.Visual.UserInterface
{
private readonly int loadDelay;
public bool DelayedLoadFinished;
public TestUpdateableOnlineBeatmapSetCover(int loadDelay = 10000)
{
this.loadDelay = loadDelay;
}
protected override void OnLoadFinished()
{
base.OnLoadFinished();
DelayedLoadFinished = true;
}
protected override Drawable CreateDrawable(IBeatmapSetOnlineInfo model)
{
if (model == null)
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using NUnit.Framework;
using osu.Game.Tests.Visual;
using osu.Game.Tournament.Components;
@@ -13,7 +11,7 @@ namespace osu.Game.Tournament.Tests.Components
{
public partial class TestSceneDateTextBox : OsuManualInputManagerTestScene
{
private DateTextBox textBox;
private DateTextBox textBox = null!;
[SetUp]
public void Setup() => Schedule(() =>
@@ -1,8 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Tournament.Models;
using osu.Game.Tournament.Screens.Ladder.Components;
@@ -10,60 +13,67 @@ namespace osu.Game.Tournament.Tests.Components
{
public partial class TestSceneDrawableTournamentMatch : TournamentTestScene
{
public TestSceneDrawableTournamentMatch()
[Test]
public void TestBasic()
{
Container<DrawableTournamentMatch> level1;
Container<DrawableTournamentMatch> level2;
Container<DrawableTournamentMatch> level1 = null!;
Container<DrawableTournamentMatch> level2 = null!;
var match1 = new TournamentMatch(
new TournamentTeam { FlagName = { Value = "AU" }, FullName = { Value = "Australia" }, },
new TournamentTeam { FlagName = { Value = "JP" }, FullName = { Value = "Japan" }, Acronym = { Value = "JPN" } })
TournamentMatch match1 = null!;
TournamentMatch match2 = null!;
AddStep("setup test", () =>
{
Team1Score = { Value = 4 },
Team2Score = { Value = 1 },
};
var match2 = new TournamentMatch(
new TournamentTeam
match1 = new TournamentMatch(
new TournamentTeam { FlagName = { Value = "AU" }, FullName = { Value = "Australia" }, },
new TournamentTeam { FlagName = { Value = "JP" }, FullName = { Value = "Japan" }, Acronym = { Value = "JPN" } })
{
FlagName = { Value = "RO" },
FullName = { Value = "Romania" },
}
);
Team1Score = { Value = 4 },
Team2Score = { Value = 1 },
};
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Children = new Drawable[]
match2 = new TournamentMatch(
new TournamentTeam
{
FlagName = { Value = "RO" },
FullName = { Value = "Romania" },
}
);
Child = new FillFlowContainer
{
level1 = new FillFlowContainer<DrawableTournamentMatch>
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
AutoSizeAxes = Axes.X,
Direction = FillDirection.Vertical,
Children = new[]
level1 = new FillFlowContainer<DrawableTournamentMatch>
{
new DrawableTournamentMatch(match1),
new DrawableTournamentMatch(match2),
new DrawableTournamentMatch(new TournamentMatch()),
}
},
level2 = new FillFlowContainer<DrawableTournamentMatch>
{
AutoSizeAxes = Axes.X,
Direction = FillDirection.Vertical,
Margin = new MarginPadding(20),
Children = new[]
AutoSizeAxes = Axes.X,
Direction = FillDirection.Vertical,
Children = new[]
{
new DrawableTournamentMatch(match1),
new DrawableTournamentMatch(match2),
new DrawableTournamentMatch(new TournamentMatch()),
}
},
level2 = new FillFlowContainer<DrawableTournamentMatch>
{
new DrawableTournamentMatch(new TournamentMatch()),
new DrawableTournamentMatch(new TournamentMatch())
AutoSizeAxes = Axes.X,
Direction = FillDirection.Vertical,
Margin = new MarginPadding(20),
Children = new[]
{
new DrawableTournamentMatch(new TournamentMatch()),
new DrawableTournamentMatch(new TournamentMatch())
}
}
}
}
};
};
level1.Children[0].Match.Progression.Value = level2.Children[0].Match;
level1.Children[1].Match.Progression.Value = level2.Children[0].Match;
level1.Children[0].Match.Progression.Value = level2.Children[0].Match;
level1.Children[1].Match.Progression.Value = level2.Children[0].Match;
});
AddRepeatStep("change scores", () => match1.Team2Score.Value++, 4);
AddStep("add new team", () => match2.Team2.Value = new TournamentTeam { FlagName = { Value = "PT" }, FullName = { Value = "Portugal" } });
@@ -78,6 +88,9 @@ namespace osu.Game.Tournament.Tests.Components
AddRepeatStep("change scores", () => level2.Children[0].Match.Team1Score.Value++, 5);
AddRepeatStep("change scores", () => level2.Children[0].Match.Team2Score.Value++, 4);
AddStep("select as current", () => match1.Current.Value = true);
AddStep("select as editing", () => this.ChildrenOfType<DrawableTournamentMatch>().Last().Selected = true);
}
}
}
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
@@ -22,7 +20,7 @@ namespace osu.Game.Tournament.Tests.Components
[Test]
public void TestSongBar()
{
SongBar songBar = null;
SongBar songBar = null!;
AddStep("create bar", () => Child = songBar = new SongBar
{
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -19,12 +17,12 @@ namespace osu.Game.Tournament.Tests.Components
public partial class TestSceneTournamentModDisplay : TournamentTestScene
{
[Resolved]
private IAPIProvider api { get; set; }
private IAPIProvider api { get; set; } = null!;
[Resolved]
private IRulesetStore rulesets { get; set; }
private IRulesetStore rulesets { get; set; } = null!;
private FillFlowContainer<TournamentBeatmapPanel> fillFlow;
private FillFlowContainer<TournamentBeatmapPanel> fillFlow = null!;
[BackgroundDependencyLoader]
private void load()
@@ -45,7 +43,7 @@ namespace osu.Game.Tournament.Tests.Components
private void success(APIBeatmap beatmap)
{
var ruleset = rulesets.GetRuleset(Ladder.Ruleset.Value.OnlineID);
var ruleset = rulesets.GetRuleset(Ladder.Ruleset.Value?.OnlineID ?? -1);
if (ruleset == null)
return;
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.IO;
using System.Threading.Tasks;
@@ -81,11 +79,11 @@ namespace osu.Game.Tournament.Tests.NonVisual
public partial class TestTournament : TournamentGameBase
{
private readonly bool resetRuleset;
private readonly Action runOnLoadComplete;
private readonly Action? runOnLoadComplete;
public new Task BracketLoadTask => base.BracketLoadTask;
public TestTournament(bool resetRuleset = false, [InstantHandle] Action runOnLoadComplete = null)
public TestTournament(bool resetRuleset = false, [InstantHandle] Action? runOnLoadComplete = null)
{
this.resetRuleset = resetRuleset;
this.runOnLoadComplete = runOnLoadComplete;
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.IO;
using System.Linq;
using NUnit.Framework;
@@ -36,11 +34,11 @@ namespace osu.Game.Tournament.Tests.NonVisual
{
var osu = LoadTournament(host);
TournamentStorage storage = (TournamentStorage)osu.Dependencies.Get<Storage>();
FileBasedIPC ipc = null;
FileBasedIPC? ipc = null;
WaitForOrAssert(() => (ipc = osu.Dependencies.Get<MatchIPCInfo>() as FileBasedIPC)?.IsLoaded == true, @"ipc could not be populated in a reasonable amount of time");
Assert.True(ipc.SetIPCLocation(testStableInstallDirectory));
Assert.True(ipc!.SetIPCLocation(testStableInstallDirectory));
Assert.True(storage.AllTournaments.Exists("stable.json"));
}
finally
@@ -35,8 +35,8 @@ namespace osu.Game.Tournament.Tests.NonVisual
PlayersPerTeam = { Value = 4 },
Teams =
{
match.Team1.Value,
match.Team2.Value,
match.Team1.Value!,
match.Team2.Value!,
},
Rounds =
{
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Threading;
using System.Threading.Tasks;
@@ -13,7 +11,7 @@ namespace osu.Game.Tournament.Tests.NonVisual
{
public abstract class TournamentHostTest
{
public static TournamentGameBase LoadTournament(GameHost host, TournamentGameBase tournament = null)
public static TournamentGameBase LoadTournament(GameHost host, TournamentGameBase? tournament = null)
{
tournament ??= new TournamentGameBase();
Task.Factory.StartNew(() => host.Run(tournament), TaskCreationOptions.LongRunning)
@@ -10,7 +10,7 @@ using osu.Game.Tournament.Screens.Drawings;
namespace osu.Game.Tournament.Tests.Screens
{
public partial class TestSceneDrawingsScreen : TournamentTestScene
public partial class TestSceneDrawingsScreen : TournamentScreenTestScene
{
[BackgroundDependencyLoader]
private void load(Storage storage)
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
@@ -15,11 +13,25 @@ using osu.Game.Tournament.Screens.Gameplay.Components;
namespace osu.Game.Tournament.Tests.Screens
{
public partial class TestSceneGameplayScreen : TournamentTestScene
public partial class TestSceneGameplayScreen : TournamentScreenTestScene
{
[Cached]
private TournamentMatchChatDisplay chat = new TournamentMatchChatDisplay { Width = 0.5f };
[Test]
public void TestWarmup()
{
createScreen();
checkScoreVisibility(false);
toggleWarmup();
checkScoreVisibility(true);
toggleWarmup();
checkScoreVisibility(false);
}
[Test]
public void TestStartupState([Values] TourneyState state)
{
@@ -35,20 +47,6 @@ namespace osu.Game.Tournament.Tests.Screens
createScreen();
}
[Test]
public void TestWarmup()
{
createScreen();
checkScoreVisibility(false);
toggleWarmup();
checkScoreVisibility(true);
toggleWarmup();
checkScoreVisibility(false);
}
private void createScreen()
{
AddStep("setup screen", () =>
@@ -1,23 +1,100 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using System;
using System.Linq;
using Newtonsoft.Json;
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Graphics.Cursor;
using osu.Game.Tournament.Screens.Editors;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Dialog;
using osu.Game.Tournament.Screens.Editors.Components;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tournament.Tests.Screens
{
public partial class TestSceneLadderEditorScreen : TournamentTestScene
public partial class TestSceneLadderEditorScreen : TournamentScreenTestScene
{
[BackgroundDependencyLoader]
private void load()
private LadderEditorScreen ladderEditorScreen = null!;
private OsuContextMenuContainer? osuContextMenuContainer;
[SetUp]
public void Setup() => Schedule(() =>
{
Add(new OsuContextMenuContainer
ladderEditorScreen = new LadderEditorScreen();
Add(osuContextMenuContainer = new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.Both,
Child = new LadderEditorScreen()
Child = ladderEditorScreen = new LadderEditorScreen()
});
});
[Test]
public void TestResetBracketTeamsCancelled()
{
Bindable<string> matchBeforeReset = new Bindable<string>();
AddStep("save current match state", () =>
{
matchBeforeReset.Value = JsonConvert.SerializeObject(Ladder.CurrentMatch.Value);
});
AddStep("pull up context menu", () =>
{
InputManager.MoveMouseTo(ladderEditorScreen);
InputManager.Click(MouseButton.Right);
});
AddStep("click Reset teams button", () =>
{
InputManager.MoveMouseTo(osuContextMenuContainer.ChildrenOfType<DrawableOsuMenuItem>().Last(p =>
((OsuMenuItem)p.Item).Type == MenuItemType.Destructive), new Vector2(5, 0));
InputManager.Click(MouseButton.Left);
});
AddAssert("dialog displayed", () => DialogOverlay.CurrentDialog is LadderResetTeamsDialog);
AddStep("click cancel", () =>
{
InputManager.MoveMouseTo(DialogOverlay.CurrentDialog.ChildrenOfType<PopupDialogButton>().Last());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("dialog dismissed", () => DialogOverlay.CurrentDialog is not LadderResetTeamsDialog);
AddAssert("assert ladder teams unchanged", () => string.Equals(matchBeforeReset.Value, JsonConvert.SerializeObject(Ladder.CurrentMatch.Value), StringComparison.Ordinal));
}
[Test]
public void TestResetBracketTeams()
{
AddStep("pull up context menu", () =>
{
InputManager.MoveMouseTo(ladderEditorScreen);
InputManager.Click(MouseButton.Right);
});
AddStep("click Reset teams button", () =>
{
InputManager.MoveMouseTo(osuContextMenuContainer.ChildrenOfType<DrawableOsuMenuItem>().Last(p =>
((OsuMenuItem)p.Item).Type == MenuItemType.Destructive), new Vector2(5, 0));
InputManager.Click(MouseButton.Left);
});
AddAssert("dialog displayed", () => DialogOverlay.CurrentDialog is LadderResetTeamsDialog);
AddStep("click confirmation", () =>
{
InputManager.MoveMouseTo(DialogOverlay.CurrentDialog.ChildrenOfType<PopupDialogButton>().First());
InputManager.PressButton(MouseButton.Left);
});
AddUntilStep("dialog dismissed", () => DialogOverlay.CurrentDialog is not LadderResetTeamsDialog);
AddStep("release mouse button", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("assert ladder teams reset", () => Ladder.CurrentMatch.Value?.Team1.Value == null && Ladder.CurrentMatch.Value?.Team2.Value == null);
}
}
}
@@ -8,7 +8,7 @@ using osu.Game.Tournament.Screens.Ladder;
namespace osu.Game.Tournament.Tests.Screens
{
public partial class TestSceneLadderScreen : TournamentTestScene
public partial class TestSceneLadderScreen : TournamentScreenTestScene
{
[BackgroundDependencyLoader]
private void load()
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
@@ -14,9 +12,9 @@ using osu.Game.Tournament.Screens.MapPool;
namespace osu.Game.Tournament.Tests.Screens
{
public partial class TestSceneMapPoolScreen : TournamentTestScene
public partial class TestSceneMapPoolScreen : TournamentScreenTestScene
{
private MapPoolScreen screen;
private MapPoolScreen screen = null!;
[BackgroundDependencyLoader]
private void load()
@@ -32,7 +30,7 @@ namespace osu.Game.Tournament.Tests.Screens
{
AddStep("load few maps", () =>
{
Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Clear();
Ladder.CurrentMatch.Value!.Round.Value!.Beatmaps.Clear();
for (int i = 0; i < 8; i++)
addBeatmap();
@@ -52,7 +50,7 @@ namespace osu.Game.Tournament.Tests.Screens
{
AddStep("load just enough maps", () =>
{
Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Clear();
Ladder.CurrentMatch.Value!.Round.Value!.Beatmaps.Clear();
for (int i = 0; i < 18; i++)
addBeatmap();
@@ -72,7 +70,7 @@ namespace osu.Game.Tournament.Tests.Screens
{
AddStep("load many maps", () =>
{
Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Clear();
Ladder.CurrentMatch.Value!.Round.Value!.Beatmaps.Clear();
for (int i = 0; i < 19; i++)
addBeatmap();
@@ -92,7 +90,7 @@ namespace osu.Game.Tournament.Tests.Screens
{
AddStep("load many maps", () =>
{
Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Clear();
Ladder.CurrentMatch.Value!.Round.Value!.Beatmaps.Clear();
for (int i = 0; i < 11; i++)
addBeatmap(i > 4 ? Ruleset.Value.CreateInstance().AllMods.ElementAt(i).Acronym : "NM");
@@ -118,7 +116,7 @@ namespace osu.Game.Tournament.Tests.Screens
{
AddStep("load many maps", () =>
{
Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Clear();
Ladder.CurrentMatch.Value!.Round.Value!.Beatmaps.Clear();
for (int i = 0; i < 12; i++)
addBeatmap(i > 4 ? Ruleset.Value.CreateInstance().AllMods.ElementAt(i).Acronym : "NM");
@@ -138,7 +136,7 @@ namespace osu.Game.Tournament.Tests.Screens
{
AddStep("load many maps", () =>
{
Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Clear();
Ladder.CurrentMatch.Value!.Round.Value!.Beatmaps.Clear();
for (int i = 0; i < 12; i++)
addBeatmap(i > 4 ? Ruleset.Value.CreateInstance().AllMods.ElementAt(i).Acronym : "NM");
@@ -155,7 +153,7 @@ namespace osu.Game.Tournament.Tests.Screens
private void addBeatmap(string mods = "NM")
{
Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Add(new RoundBeatmap
Ladder.CurrentMatch.Value!.Round.Value!.Beatmaps.Add(new RoundBeatmap
{
Beatmap = CreateSampleBeatmap(),
Mods = mods
@@ -1,13 +1,15 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Game.Tournament.Screens.Editors;
namespace osu.Game.Tournament.Tests.Screens
{
public partial class TestSceneRoundEditorScreen : TournamentTestScene
public partial class TestSceneRoundEditorScreen : TournamentScreenTestScene
{
public TestSceneRoundEditorScreen()
[BackgroundDependencyLoader]
private void load()
{
Add(new RoundEditorScreen
{
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using NUnit.Framework;
using osu.Framework.Allocation;
@@ -12,7 +10,7 @@ using osu.Game.Tournament.Screens.Schedule;
namespace osu.Game.Tournament.Tests.Screens
{
public partial class TestSceneScheduleScreen : TournamentTestScene
public partial class TestSceneScheduleScreen : TournamentScreenTestScene
{
[BackgroundDependencyLoader]
private void load()
@@ -2,21 +2,23 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Game.Tournament.Models;
using osu.Game.Tournament.Screens.Editors;
namespace osu.Game.Tournament.Tests.Screens
{
public partial class TestSceneSeedingEditorScreen : TournamentTestScene
public partial class TestSceneSeedingEditorScreen : TournamentScreenTestScene
{
[Cached]
private readonly LadderInfo ladder = new LadderInfo();
public TestSceneSeedingEditorScreen()
[BackgroundDependencyLoader]
private void load()
{
var match = CreateSampleMatch();
Add(new SeedingEditorScreen(match.Team1.Value, new TeamEditorScreen())
Add(new SeedingEditorScreen(match.Team1.Value.AsNonNull(), new TeamEditorScreen())
{
Width = 0.85f // create room for control panel
});
@@ -12,7 +12,7 @@ using osu.Game.Tournament.Screens.TeamIntro;
namespace osu.Game.Tournament.Tests.Screens
{
public partial class TestSceneSeedingScreen : TournamentTestScene
public partial class TestSceneSeedingScreen : TournamentScreenTestScene
{
[Cached]
private readonly LadderInfo ladder = new LadderInfo
@@ -6,7 +6,7 @@ using osu.Game.Tournament.Screens.Setup;
namespace osu.Game.Tournament.Tests.Screens
{
public partial class TestSceneSetupScreen : TournamentTestScene
public partial class TestSceneSetupScreen : TournamentScreenTestScene
{
[BackgroundDependencyLoader]
private void load()
@@ -6,7 +6,7 @@ using osu.Game.Tournament.Screens.Showcase;
namespace osu.Game.Tournament.Tests.Screens
{
public partial class TestSceneShowcaseScreen : TournamentTestScene
public partial class TestSceneShowcaseScreen : TournamentScreenTestScene
{
[BackgroundDependencyLoader]
private void load()
@@ -5,7 +5,7 @@ using osu.Game.Tournament.Screens.Setup;
namespace osu.Game.Tournament.Tests.Screens
{
public partial class TestSceneStablePathSelectScreen : TournamentTestScene
public partial class TestSceneStablePathSelectScreen : TournamentScreenTestScene
{
public TestSceneStablePathSelectScreen()
{
@@ -1,13 +1,15 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Game.Tournament.Screens.Editors;
namespace osu.Game.Tournament.Tests.Screens
{
public partial class TestSceneTeamEditorScreen : TournamentTestScene
public partial class TestSceneTeamEditorScreen : TournamentScreenTestScene
{
public TestSceneTeamEditorScreen()
[BackgroundDependencyLoader]
private void load()
{
Add(new TeamEditorScreen
{
@@ -9,7 +9,7 @@ using osu.Game.Tournament.Screens.TeamIntro;
namespace osu.Game.Tournament.Tests.Screens
{
public partial class TestSceneTeamIntroScreen : TournamentTestScene
public partial class TestSceneTeamIntroScreen : TournamentScreenTestScene
{
[Cached]
private readonly LadderInfo ladder = new LadderInfo();
@@ -8,7 +8,7 @@ using osu.Game.Tournament.Screens.TeamWin;
namespace osu.Game.Tournament.Tests.Screens
{
public partial class TestSceneTeamWinScreen : TournamentTestScene
public partial class TestSceneTeamWinScreen : TournamentScreenTestScene
{
[Test]
public void TestBasic()
@@ -17,7 +17,7 @@ namespace osu.Game.Tournament.Tests.Screens
{
var match = Ladder.CurrentMatch.Value!;
match.Round.Value = Ladder.Rounds.FirstOrDefault(g => g.Name.Value == "Finals");
match.Round.Value = Ladder.Rounds.First(g => g.Name.Value == "Quarterfinals");
match.Completed.Value = true;
});
@@ -0,0 +1,38 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osuTK;
namespace osu.Game.Tournament.Tests
{
public abstract partial class TournamentScreenTestScene : TournamentTestScene
{
protected override Container<Drawable> Content { get; } = new TournamentScalingContainer();
[BackgroundDependencyLoader]
private void load()
{
base.Content.Add(Content);
}
private partial class TournamentScalingContainer : DrawSizePreservingFillContainer
{
public TournamentScalingContainer()
{
TargetDrawSize = new Vector2(1920, 1080);
RelativeSizeAxes = Axes.Both;
}
protected override void Update()
{
base.Update();
Scale = new Vector2(Math.Min(1, Content.DrawWidth / (1920 + TournamentSceneManager.CONTROL_AREA_WIDTH)));
}
}
}
}
@@ -12,7 +12,7 @@ namespace osu.Game.Tournament.Tests
[STAThread]
public static int Main(string[] args)
{
using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu", new HostOptions { BindIPC = true }))
using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu-development", new HostOptions { BindIPC = true }))
{
host.Run(new TournamentTestBrowser());
return 0;
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Linq;
using System.Threading;
using osu.Framework.Allocation;
@@ -10,6 +8,7 @@ using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Overlays;
using osu.Game.Rulesets;
using osu.Game.Tests.Visual;
using osu.Game.Tournament.IO;
@@ -18,19 +17,22 @@ using osu.Game.Tournament.Models;
namespace osu.Game.Tournament.Tests
{
public abstract partial class TournamentTestScene : OsuTestScene
public abstract partial class TournamentTestScene : OsuManualInputManagerTestScene
{
private TournamentMatch match;
[Cached(typeof(IDialogOverlay))]
protected readonly DialogOverlay DialogOverlay = new DialogOverlay { Depth = float.MinValue };
[Cached]
protected LadderInfo Ladder { get; private set; } = new LadderInfo();
[Resolved]
private RulesetStore rulesetStore { get; set; }
[Cached]
protected MatchIPCInfo IPCInfo { get; private set; } = new MatchIPCInfo();
[Resolved]
private RulesetStore rulesetStore { get; set; } = null!;
private TournamentMatch match = null!;
[BackgroundDependencyLoader]
private void load(TournamentStorage storage)
{
@@ -38,13 +40,15 @@ namespace osu.Game.Tournament.Tests
match = CreateSampleMatch();
Ladder.Rounds.Add(match.Round.Value);
Ladder.Rounds.Add(match.Round.Value!);
Ladder.Matches.Add(match);
Ladder.Teams.Add(match.Team1.Value);
Ladder.Teams.Add(match.Team2.Value);
Ladder.Teams.Add(match.Team1.Value!);
Ladder.Teams.Add(match.Team2.Value!);
Ruleset.BindTo(Ladder.Ruleset);
Dependencies.CacheAs(new StableInfo(storage));
Add(DialogOverlay);
}
[SetUpSteps]
@@ -148,7 +152,7 @@ namespace osu.Game.Tournament.Tests
},
Round =
{
Value = new TournamentRound { Name = { Value = "Quarterfinals" } }
Value = new TournamentRound { Name = { Value = "Quarterfinals" } },
}
};
@@ -167,7 +171,7 @@ namespace osu.Game.Tournament.Tests
public partial class TournamentTestSceneTestRunner : TournamentGameBase, ITestSceneTestRunner
{
private TestSceneTestRunner.TestRunner runner;
private TestSceneTestRunner.TestRunner runner = null!;
protected override void LoadAsyncComplete()
{
+7 -12
View File
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using osu.Framework.Bindables;
using osu.Game.Graphics.UserInterface;
@@ -12,24 +10,21 @@ namespace osu.Game.Tournament.Components
{
public partial class DateTextBox : SettingsTextBox
{
public new Bindable<DateTimeOffset> Current
private readonly BindableWithCurrent<DateTimeOffset> current = new BindableWithCurrent<DateTimeOffset>();
public new Bindable<DateTimeOffset>? Current
{
get => current;
set
{
current = value.GetBoundCopy();
current.BindValueChanged(dto =>
base.Current.Value = dto.NewValue.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ"), true);
}
set => current.Current = value;
}
// hold a reference to the provided bindable so we don't have to in every settings section.
private Bindable<DateTimeOffset> current = new Bindable<DateTimeOffset>();
public DateTextBox()
{
base.Current = new Bindable<string>(string.Empty);
current.BindValueChanged(dto =>
base.Current.Value = dto.NewValue.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ"), true);
((OsuTextBox)Control).OnCommit += (sender, _) =>
{
try
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -17,14 +15,14 @@ namespace osu.Game.Tournament.Components
{
public partial class DrawableTeamFlag : Container
{
private readonly TournamentTeam team;
private readonly TournamentTeam? team;
[UsedImplicitly]
private Bindable<string> flag;
private Bindable<string>? flag;
private Sprite flagSprite;
private Sprite? flagSprite;
public DrawableTeamFlag(TournamentTeam team)
public DrawableTeamFlag(TournamentTeam? team)
{
this.team = team;
}
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -12,12 +10,12 @@ namespace osu.Game.Tournament.Components
{
public partial class DrawableTeamTitle : TournamentSpriteTextWithBackground
{
private readonly TournamentTeam team;
private readonly TournamentTeam? team;
[UsedImplicitly]
private Bindable<string> acronym;
private Bindable<string>? acronym;
public DrawableTeamTitle(TournamentTeam team)
public DrawableTeamTitle(TournamentTeam? team)
{
this.team = team;
}
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -14,15 +12,15 @@ namespace osu.Game.Tournament.Components
{
public abstract partial class DrawableTournamentTeam : CompositeDrawable
{
public readonly TournamentTeam Team;
public readonly TournamentTeam? Team;
protected readonly Container Flag;
protected readonly TournamentSpriteText AcronymText;
[UsedImplicitly]
private Bindable<string> acronym;
private Bindable<string>? acronym;
protected DrawableTournamentTeam(TournamentTeam team)
protected DrawableTournamentTeam(TournamentTeam? team)
{
Team = team;
@@ -36,7 +34,8 @@ namespace osu.Game.Tournament.Components
[BackgroundDependencyLoader]
private void load()
{
if (Team == null) return;
if (Team == null)
return;
(acronym = Team.Acronym.GetBoundCopy()).BindValueChanged(_ => AcronymText.Text = Team?.Acronym.Value?.ToUpperInvariant() ?? string.Empty, true);
}
+4 -6
View File
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -24,14 +22,14 @@ namespace osu.Game.Tournament.Components
{
public partial class SongBar : CompositeDrawable
{
private TournamentBeatmap beatmap;
private TournamentBeatmap? beatmap;
public const float HEIGHT = 145 / 2f;
[Resolved]
private IBindable<RulesetInfo> ruleset { get; set; }
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
public TournamentBeatmap Beatmap
public TournamentBeatmap? Beatmap
{
set
{
@@ -55,7 +53,7 @@ namespace osu.Game.Tournament.Components
}
}
private FillFlowContainer flow;
private FillFlowContainer flow = null!;
private bool expanded;
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Collections.Specialized;
using System.Linq;
@@ -11,6 +9,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics;
@@ -21,19 +20,18 @@ namespace osu.Game.Tournament.Components
{
public partial class TournamentBeatmapPanel : CompositeDrawable
{
public readonly TournamentBeatmap Beatmap;
public readonly TournamentBeatmap? Beatmap;
private readonly string mod;
public const float HEIGHT = 50;
private readonly Bindable<TournamentMatch> currentMatch = new Bindable<TournamentMatch>();
private Box flash;
private readonly Bindable<TournamentMatch?> currentMatch = new Bindable<TournamentMatch?>();
public TournamentBeatmapPanel(TournamentBeatmap beatmap, string mod = null)
private Box flash = null!;
public TournamentBeatmapPanel(TournamentBeatmap? beatmap, string mod = "")
{
ArgumentNullException.ThrowIfNull(beatmap);
Beatmap = beatmap;
this.mod = mod;
@@ -56,7 +54,7 @@ namespace osu.Game.Tournament.Components
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
},
new UpdateableOnlineBeatmapSetCover
new NoUnloadBeatmapSetCover
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.Gray(0.5f),
@@ -73,7 +71,7 @@ namespace osu.Game.Tournament.Components
{
new TournamentSpriteText
{
Text = Beatmap.GetDisplayTitleRomanisable(false, false),
Text = Beatmap?.GetDisplayTitleRomanisable(false, false) ?? (LocalisableString)@"unknown",
Font = OsuFont.Torus.With(weight: FontWeight.Bold),
},
new FillFlowContainer
@@ -90,7 +88,7 @@ namespace osu.Game.Tournament.Components
},
new TournamentSpriteText
{
Text = Beatmap.Metadata.Author.Username,
Text = Beatmap?.Metadata.Author.Username ?? "unknown",
Padding = new MarginPadding { Right = 20 },
Font = OsuFont.Torus.With(weight: FontWeight.Bold, size: 14)
},
@@ -102,7 +100,7 @@ namespace osu.Game.Tournament.Components
},
new TournamentSpriteText
{
Text = Beatmap.DifficultyName,
Text = Beatmap?.DifficultyName ?? "unknown",
Font = OsuFont.Torus.With(weight: FontWeight.Bold, size: 14)
},
}
@@ -131,36 +129,42 @@ namespace osu.Game.Tournament.Components
}
}
private void matchChanged(ValueChangedEvent<TournamentMatch> match)
private void matchChanged(ValueChangedEvent<TournamentMatch?> match)
{
if (match.OldValue != null)
match.OldValue.PicksBans.CollectionChanged -= picksBansOnCollectionChanged;
match.NewValue.PicksBans.CollectionChanged += picksBansOnCollectionChanged;
updateState();
if (match.NewValue != null)
match.NewValue.PicksBans.CollectionChanged += picksBansOnCollectionChanged;
Scheduler.AddOnce(updateState);
}
private void picksBansOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
=> updateState();
private void picksBansOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
=> Scheduler.AddOnce(updateState);
private BeatmapChoice choice;
private BeatmapChoice? choice;
private void updateState()
{
var found = currentMatch.Value.PicksBans.FirstOrDefault(p => p.BeatmapID == Beatmap.OnlineID);
bool doFlash = found != choice;
choice = found;
if (found != null)
if (currentMatch.Value == null)
{
if (doFlash)
flash?.FadeOutFromOne(500).Loop(0, 10);
return;
}
var newChoice = currentMatch.Value.PicksBans.FirstOrDefault(p => p.BeatmapID == Beatmap?.OnlineID);
bool shouldFlash = newChoice != choice;
if (newChoice != null)
{
if (shouldFlash)
flash.FadeOutFromOne(500).Loop(0, 10);
BorderThickness = 6;
BorderColour = TournamentGame.GetTeamColour(found.Team);
BorderColour = TournamentGame.GetTeamColour(newChoice.Team);
switch (found.Type)
switch (newChoice.Type)
{
case ChoiceType.Pick:
Colour = Color4.White;
@@ -179,6 +183,18 @@ namespace osu.Game.Tournament.Components
BorderThickness = 0;
Alpha = 1;
}
choice = newChoice;
}
private partial class NoUnloadBeatmapSetCover : UpdateableOnlineBeatmapSetCover
{
// As covers are displayed on stream, we want them to load as soon as possible.
protected override double LoadDelay => 0;
// Use DelayedLoadWrapper to avoid content unloading when switching away to another screen.
protected override DelayedLoadWrapper CreateDelayedLoadWrapper(Func<Drawable> createContentFunc, double timeBeforeLoad)
=> new DelayedLoadWrapper(createContentFunc, timeBeforeLoad);
}
}
}
@@ -92,9 +92,9 @@ namespace osu.Game.Tournament.Components
{
if (info.CurrentMatch.Value is TournamentMatch match)
{
if (match.Team1.Value.Players.Any(u => u.OnlineID == Message.Sender.OnlineID))
if (match.Team1.Value?.Players.Any(u => u.OnlineID == Message.Sender.OnlineID) == true)
UsernameColour = TournamentGame.COLOUR_RED;
else if (match.Team2.Value.Players.Any(u => u.OnlineID == Message.Sender.OnlineID))
else if (match.Team2.Value?.Players.Any(u => u.OnlineID == Message.Sender.OnlineID) == true)
UsernameColour = TournamentGame.COLOUR_BLUE;
}
}
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
@@ -19,8 +17,8 @@ namespace osu.Game.Tournament.Components
{
private readonly string filename;
private readonly bool drawFallbackGradient;
private Video video;
private ManualClock manualClock;
private Video? video;
private ManualClock? manualClock;
public bool VideoAvailable => video != null;
+3 -1
View File
@@ -1,7 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Logging;
using osu.Framework.Platform;
@@ -43,6 +45,6 @@ namespace osu.Game.Tournament.IO
Logger.Log("Changing tournament storage: " + GetFullPath(string.Empty));
}
public IEnumerable<string> ListTournaments() => AllTournaments.GetDirectories(string.Empty);
public IEnumerable<string> ListTournaments() => AllTournaments.GetDirectories(string.Empty).OrderBy(directory => directory, StringComparer.CurrentCultureIgnoreCase);
}
}
+30 -34
View File
@@ -1,12 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.IO;
using System.Linq;
using JetBrains.Annotations;
using Microsoft.Win32;
using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
@@ -24,36 +21,35 @@ namespace osu.Game.Tournament.IPC
{
public partial class FileBasedIPC : MatchIPCInfo
{
public Storage IPCStorage { get; private set; }
public Storage? IPCStorage { get; private set; }
[Resolved]
protected IAPIProvider API { get; private set; }
protected IAPIProvider API { get; private set; } = null!;
[Resolved]
protected IRulesetStore Rulesets { get; private set; }
protected IRulesetStore Rulesets { get; private set; } = null!;
[Resolved]
private GameHost host { get; set; }
private GameHost host { get; set; } = null!;
[Resolved]
private LadderInfo ladder { get; set; }
private LadderInfo ladder { get; set; } = null!;
[Resolved]
private StableInfo stableInfo { get; set; }
private StableInfo stableInfo { get; set; } = null!;
private int lastBeatmapId;
private ScheduledDelegate scheduled;
private GetBeatmapRequest beatmapLookupRequest;
private ScheduledDelegate? scheduled;
private GetBeatmapRequest? beatmapLookupRequest;
[BackgroundDependencyLoader]
private void load()
{
string stablePath = stableInfo.StablePath ?? findStablePath();
string? stablePath = stableInfo.StablePath ?? findStablePath();
initialiseIPCStorage(stablePath);
}
[CanBeNull]
private Storage initialiseIPCStorage(string path)
private Storage? initialiseIPCStorage(string? path)
{
scheduled?.Cancel();
@@ -89,7 +85,7 @@ namespace osu.Game.Tournament.IPC
lastBeatmapId = beatmapId;
var existing = ladder.CurrentMatch.Value?.Round.Value?.Beatmaps.FirstOrDefault(b => b.ID == beatmapId && b.Beatmap != null);
var existing = ladder.CurrentMatch.Value?.Round.Value?.Beatmaps.FirstOrDefault(b => b.ID == beatmapId);
if (existing != null)
Beatmap.Value = existing.Beatmap;
@@ -97,6 +93,7 @@ namespace osu.Game.Tournament.IPC
{
beatmapLookupRequest = new GetBeatmapRequest(new APIBeatmap { OnlineID = beatmapId });
beatmapLookupRequest.Success += b => Beatmap.Value = new TournamentBeatmap(b);
beatmapLookupRequest.Failure += _ => Beatmap.Value = null;
API.Queue(beatmapLookupRequest);
}
}
@@ -114,7 +111,7 @@ namespace osu.Game.Tournament.IPC
using (var stream = IPCStorage.GetStream(file_ipc_channel_filename))
using (var sr = new StreamReader(stream))
{
ChatChannel.Value = sr.ReadLine();
ChatChannel.Value = sr.ReadLine().AsNonNull();
}
}
catch (Exception)
@@ -140,8 +137,8 @@ namespace osu.Game.Tournament.IPC
using (var stream = IPCStorage.GetStream(file_ipc_scores_filename))
using (var sr = new StreamReader(stream))
{
Score1.Value = int.Parse(sr.ReadLine());
Score2.Value = int.Parse(sr.ReadLine());
Score1.Value = int.Parse(sr.ReadLine().AsNonNull());
Score2.Value = int.Parse(sr.ReadLine().AsNonNull());
}
}
catch (Exception)
@@ -164,7 +161,7 @@ namespace osu.Game.Tournament.IPC
/// </summary>
/// <param name="path">Path to the IPC directory</param>
/// <returns>Whether the supplied path was a valid IPC directory.</returns>
public bool SetIPCLocation(string path)
public bool SetIPCLocation(string? path)
{
if (path == null || !ipcFileExistsInDirectory(path))
return false;
@@ -184,29 +181,28 @@ namespace osu.Game.Tournament.IPC
/// <returns>Whether an IPC directory was successfully auto-detected.</returns>
public bool AutoDetectIPCLocation() => SetIPCLocation(findStablePath());
private static bool ipcFileExistsInDirectory(string p) => p != null && File.Exists(Path.Combine(p, "ipc.txt"));
private static bool ipcFileExistsInDirectory(string? p) => p != null && File.Exists(Path.Combine(p, "ipc.txt"));
[CanBeNull]
private string findStablePath()
private string? findStablePath()
{
string stableInstallPath = findFromEnvVar() ??
findFromRegistry() ??
findFromLocalAppData() ??
findFromDotFolder();
string? stableInstallPath = findFromEnvVar() ??
findFromRegistry() ??
findFromLocalAppData() ??
findFromDotFolder();
Logger.Log($"Stable path for tourney usage: {stableInstallPath}");
return stableInstallPath;
}
private string findFromEnvVar()
private string? findFromEnvVar()
{
try
{
Logger.Log("Trying to find stable with environment variables");
string stableInstallPath = Environment.GetEnvironmentVariable("OSU_STABLE_PATH");
string? stableInstallPath = Environment.GetEnvironmentVariable("OSU_STABLE_PATH");
if (ipcFileExistsInDirectory(stableInstallPath))
return stableInstallPath;
return stableInstallPath!;
}
catch
{
@@ -215,7 +211,7 @@ namespace osu.Game.Tournament.IPC
return null;
}
private string findFromLocalAppData()
private string? findFromLocalAppData()
{
Logger.Log("Trying to find stable in %LOCALAPPDATA%");
string stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"osu!");
@@ -226,7 +222,7 @@ namespace osu.Game.Tournament.IPC
return null;
}
private string findFromDotFolder()
private string? findFromDotFolder()
{
Logger.Log("Trying to find stable in dotfolders");
string stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".osu");
@@ -237,16 +233,16 @@ namespace osu.Game.Tournament.IPC
return null;
}
private string findFromRegistry()
private string? findFromRegistry()
{
Logger.Log("Trying to find stable in registry");
try
{
string stableInstallPath;
string? stableInstallPath;
#pragma warning disable CA1416
using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu"))
using (RegistryKey? key = Registry.ClassesRoot.OpenSubKey("osu"))
stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty)?.ToString()?.Split('"')[1].Replace("osu!.exe", "");
#pragma warning restore CA1416
+1 -1
View File
@@ -10,7 +10,7 @@ namespace osu.Game.Tournament.IPC
{
public partial class MatchIPCInfo : Component
{
public Bindable<TournamentBeatmap> Beatmap { get; } = new Bindable<TournamentBeatmap>();
public Bindable<TournamentBeatmap?> Beatmap { get; } = new Bindable<TournamentBeatmap?>();
public Bindable<LegacyMods> Mods { get; } = new Bindable<LegacyMods>();
public Bindable<TourneyState> State { get; } = new Bindable<TourneyState>();
public Bindable<string> ChatChannel { get; } = new Bindable<string>();
+5 -4
View File
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Diagnostics;
using System.Drawing;
@@ -28,7 +26,7 @@ namespace osu.Game.Tournament
if (reader.TokenType != JsonToken.StartObject)
{
// if there's no object present then this is using string representation (System.Drawing.Point serializes to "x,y")
string str = (string)reader.Value;
string? str = (string?)reader.Value;
Debug.Assert(str != null);
@@ -45,9 +43,12 @@ namespace osu.Game.Tournament
if (reader.TokenType == JsonToken.PropertyName)
{
string name = reader.Value?.ToString();
string? name = reader.Value?.ToString();
int? val = reader.ReadAsInt32();
if (name == null)
continue;
if (val == null)
continue;
+2 -4
View File
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
@@ -17,7 +15,7 @@ namespace osu.Game.Tournament.Models
[Serializable]
public class LadderInfo
{
public Bindable<RulesetInfo> Ruleset = new Bindable<RulesetInfo>();
public Bindable<RulesetInfo?> Ruleset = new Bindable<RulesetInfo?>();
public BindableList<TournamentMatch> Matches = new BindableList<TournamentMatch>();
public BindableList<TournamentRound> Rounds = new BindableList<TournamentRound>();
@@ -27,7 +25,7 @@ namespace osu.Game.Tournament.Models
public List<TournamentProgression> Progressions = new List<TournamentProgression>();
[JsonIgnore] // updated manually in TournamentGameBase
public Bindable<TournamentMatch> CurrentMatch = new Bindable<TournamentMatch>();
public Bindable<TournamentMatch?> CurrentMatch = new Bindable<TournamentMatch?>();
public Bindable<int> ChromaKeyWidth = new BindableInt(1024)
{
+2 -4
View File
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using Newtonsoft.Json;
namespace osu.Game.Tournament.Models
@@ -10,9 +8,9 @@ namespace osu.Game.Tournament.Models
public class RoundBeatmap
{
public int ID;
public string Mods;
public string Mods = string.Empty;
[JsonProperty("BeatmapInfo")]
public TournamentBeatmap Beatmap;
public TournamentBeatmap? Beatmap;
}
}
+2 -4
View File
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.IO;
using Newtonsoft.Json;
@@ -20,12 +18,12 @@ namespace osu.Game.Tournament.Models
/// <summary>
/// Path to the IPC directory used by the stable (cutting-edge) install.
/// </summary>
public string StablePath { get; set; }
public string? StablePath { get; set; }
/// <summary>
/// Fired whenever stable info is successfully saved to file.
/// </summary>
public event Action OnStableInfoSaved;
public event Action? OnStableInfoSaved;
private const string config_path = "stable.json";
+10 -12
View File
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
@@ -33,16 +31,16 @@ namespace osu.Game.Tournament.Models
}
[JsonIgnore]
public readonly Bindable<TournamentTeam> Team1 = new Bindable<TournamentTeam>();
public readonly Bindable<TournamentTeam?> Team1 = new Bindable<TournamentTeam?>();
public string Team1Acronym;
public string? Team1Acronym;
public readonly Bindable<int?> Team1Score = new Bindable<int?>();
[JsonIgnore]
public readonly Bindable<TournamentTeam> Team2 = new Bindable<TournamentTeam>();
public readonly Bindable<TournamentTeam?> Team2 = new Bindable<TournamentTeam?>();
public string Team2Acronym;
public string? Team2Acronym;
public readonly Bindable<int?> Team2Score = new Bindable<int?>();
@@ -53,13 +51,13 @@ namespace osu.Game.Tournament.Models
public readonly ObservableCollection<BeatmapChoice> PicksBans = new ObservableCollection<BeatmapChoice>();
[JsonIgnore]
public readonly Bindable<TournamentRound> Round = new Bindable<TournamentRound>();
public readonly Bindable<TournamentRound?> Round = new Bindable<TournamentRound?>();
[JsonIgnore]
public readonly Bindable<TournamentMatch> Progression = new Bindable<TournamentMatch>();
public readonly Bindable<TournamentMatch?> Progression = new Bindable<TournamentMatch?>();
[JsonIgnore]
public readonly Bindable<TournamentMatch> LosersProgression = new Bindable<TournamentMatch>();
public readonly Bindable<TournamentMatch?> LosersProgression = new Bindable<TournamentMatch?>();
/// <summary>
/// Should not be set directly. Use LadderInfo.CurrentMatch.Value = this instead.
@@ -79,7 +77,7 @@ namespace osu.Game.Tournament.Models
Team2.BindValueChanged(t => Team2Acronym = t.NewValue?.Acronym.Value, true);
}
public TournamentMatch(TournamentTeam team1 = null, TournamentTeam team2 = null)
public TournamentMatch(TournamentTeam? team1 = null, TournamentTeam? team2 = null)
: this()
{
Team1.Value = team1;
@@ -87,10 +85,10 @@ namespace osu.Game.Tournament.Models
}
[JsonIgnore]
public TournamentTeam Winner => !Completed.Value ? null : Team1Score.Value > Team2Score.Value ? Team1.Value : Team2.Value;
public TournamentTeam? Winner => !Completed.Value ? null : Team1Score.Value > Team2Score.Value ? Team1.Value : Team2.Value;
[JsonIgnore]
public TournamentTeam Loser => !Completed.Value ? null : Team1Score.Value > Team2Score.Value ? Team2.Value : Team1.Value;
public TournamentTeam? Loser => !Completed.Value ? null : Team1Score.Value > Team2Score.Value ? Team2.Value : Team1.Value;
public TeamColour WinnerColour => Winner == Team1.Value ? TeamColour.Red : TeamColour.Blue;
+3 -5
View File
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Linq;
using Newtonsoft.Json;
@@ -39,7 +37,7 @@ namespace osu.Game.Tournament.Models
{
int[] ranks = Players.Select(p => p.Rank)
.Where(i => i.HasValue)
.Select(i => i.Value)
.Select(i => i!.Value)
.ToArray();
if (ranks.Length == 0)
@@ -66,14 +64,14 @@ namespace osu.Game.Tournament.Models
{
// use a sane default flag name based on acronym.
if (val.OldValue.StartsWith(FlagName.Value, StringComparison.InvariantCultureIgnoreCase))
FlagName.Value = val.NewValue.Length >= 2 ? val.NewValue?.Substring(0, 2).ToUpperInvariant() : string.Empty;
FlagName.Value = val.NewValue?.Length >= 2 ? val.NewValue.Substring(0, 2).ToUpperInvariant() : string.Empty;
};
FullName.ValueChanged += val =>
{
// use a sane acronym based on full name.
if (val.OldValue.StartsWith(Acronym.Value, StringComparison.InvariantCultureIgnoreCase))
Acronym.Value = val.NewValue.Length >= 3 ? val.NewValue?.Substring(0, 3).ToUpperInvariant() : string.Empty;
Acronym.Value = val.NewValue?.Length >= 3 ? val.NewValue.Substring(0, 3).ToUpperInvariant() : string.Empty;
};
}
+4 -13
View File
@@ -25,12 +25,11 @@ namespace osu.Game.Tournament
{
RelativeSizeAxes = Axes.Both;
InternalChild = new Container
InternalChild = new CircularContainer
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Position = new Vector2(5),
CornerRadius = 10,
Position = new Vector2(-5),
Masking = true,
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
@@ -43,18 +42,10 @@ namespace osu.Game.Tournament
saveChangesButton = new TourneyButton
{
Text = "Save Changes",
RelativeSizeAxes = Axes.None,
Width = 140,
Height = 50,
Padding = new MarginPadding
{
Top = 10,
Left = 10,
},
Margin = new MarginPadding
{
Right = 10,
Bottom = 10,
},
Margin = new MarginPadding(10),
Action = saveChanges,
// Enabled = { Value = false },
},
@@ -37,7 +37,7 @@ namespace osu.Game.Tournament.Screens
SongBar.Mods = mods.NewValue;
}
private void beatmapChanged(ValueChangedEvent<TournamentBeatmap> beatmap)
private void beatmapChanged(ValueChangedEvent<TournamentBeatmap?> beatmap)
{
SongBar.FadeInFromZero(300, Easing.OutQuint);
SongBar.Beatmap = beatmap.NewValue;
@@ -84,7 +84,7 @@ namespace osu.Game.Tournament.Screens.Drawings.Components
public bool ContainsTeam(string fullName)
{
return allTeams.Any(t => t.Team.FullName.Value == fullName);
return allTeams.Any(t => t.Team?.FullName.Value == fullName);
}
public bool RemoveTeam(TournamentTeam team)
@@ -112,7 +112,7 @@ namespace osu.Game.Tournament.Screens.Drawings.Components
{
StringBuilder sb = new StringBuilder();
foreach (GroupTeam gt in allTeams)
sb.AppendLine(gt.Team.FullName.Value);
sb.AppendLine(gt.Team?.FullName.Value);
return sb.ToString();
}
}
@@ -1,12 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
@@ -22,8 +21,8 @@ namespace osu.Game.Tournament.Screens.Drawings.Components
{
public partial class ScrollingTeamContainer : Container
{
public event Action OnScrollStarted;
public event Action<TournamentTeam> OnSelected;
public event Action? OnScrollStarted;
public event Action<TournamentTeam>? OnSelected;
private readonly List<TournamentTeam> availableTeams = new List<TournamentTeam>();
@@ -42,7 +41,7 @@ namespace osu.Game.Tournament.Screens.Drawings.Components
private double lastTime;
private ScheduledDelegate delayedStateChangeDelegate;
private ScheduledDelegate? delayedStateChangeDelegate;
public ScrollingTeamContainer()
{
@@ -117,7 +116,7 @@ namespace osu.Game.Tournament.Screens.Drawings.Components
if (!Children.Any())
break;
ScrollingTeam closest = null;
ScrollingTeam? closest = null;
foreach (var c in Children)
{
@@ -137,9 +136,8 @@ namespace osu.Game.Tournament.Screens.Drawings.Components
closest = stc;
}
Trace.Assert(closest != null, "closest != null");
Debug.Assert(closest != null, "closest != null");
// ReSharper disable once PossibleNullReferenceException
offset += DrawWidth / 2f - (closest.Position.X + closest.DrawWidth / 2f);
ScrollingTeam st = closest;
@@ -147,7 +145,7 @@ namespace osu.Game.Tournament.Screens.Drawings.Components
availableTeams.RemoveAll(at => at == st.Team);
st.Selected = true;
OnSelected?.Invoke(st.Team);
OnSelected?.Invoke(st.Team.AsNonNull());
delayedStateChangeDelegate = Scheduler.AddDelayed(() => setScrollState(ScrollState.Idle), 10000);
break;
@@ -174,7 +172,7 @@ namespace osu.Game.Tournament.Screens.Drawings.Components
setScrollState(ScrollState.Idle);
}
public void AddTeams(IEnumerable<TournamentTeam> teams)
public void AddTeams(IEnumerable<TournamentTeam>? teams)
{
if (teams == null)
return;
@@ -311,6 +309,8 @@ namespace osu.Game.Tournament.Screens.Drawings.Components
public partial class ScrollingTeam : DrawableTournamentTeam
{
public new TournamentTeam Team => base.Team.AsNonNull();
public const float WIDTH = 58;
public const float HEIGHT = 44;

Some files were not shown because too many files have changed in this diff Show More