mirror of
https://github.com/ppy/osu.git
synced 2026-05-18 14:10:33 +08:00
Merge branch 'master' into release
This commit is contained in:
@@ -15,7 +15,7 @@
|
||||
]
|
||||
},
|
||||
"codefilesanity": {
|
||||
"version": "0.0.36",
|
||||
"version": "0.0.37",
|
||||
"commands": [
|
||||
"CodeFileSanity"
|
||||
]
|
||||
|
||||
@@ -2,7 +2,7 @@ blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Help
|
||||
url: https://github.com/ppy/osu/discussions/categories/q-a
|
||||
about: osu! not working as you'd expect? Not sure it's a bug? Check the Q&A section!
|
||||
about: osu! not working or performing as you'd expect? Not sure it's a bug? Check the Q&A section!
|
||||
- name: Suggestions or feature request
|
||||
url: https://github.com/ppy/osu/discussions/categories/ideas
|
||||
about: Got something you think should change or be added? Search for or start a new discussion!
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
<AndroidManifestMerger>manifestmerger.jar</AndroidManifestMerger>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.608.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.620.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidManifestOverlay Include="$(MSBuildThisFileDirectory)osu.Android\Properties\AndroidManifestOverlay.xml" />
|
||||
|
||||
@@ -2,12 +2,10 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Win32;
|
||||
using osu.Desktop.Security;
|
||||
using osu.Framework.Platform;
|
||||
@@ -17,7 +15,6 @@ using osu.Framework;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Updater;
|
||||
using osu.Desktop.Windows;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.IPC;
|
||||
using osu.Game.Utils;
|
||||
@@ -138,52 +135,10 @@ namespace osu.Desktop
|
||||
|
||||
desktopWindow.CursorState |= CursorState.Hidden;
|
||||
desktopWindow.Title = Name;
|
||||
desktopWindow.DragDrop += f =>
|
||||
{
|
||||
// on macOS, URL associations are handled via SDL_DROPFILE events.
|
||||
if (f.StartsWith(OSU_PROTOCOL, StringComparison.Ordinal))
|
||||
{
|
||||
HandleLink(f);
|
||||
return;
|
||||
}
|
||||
|
||||
fileDrop(new[] { f });
|
||||
};
|
||||
}
|
||||
|
||||
protected override BatteryInfo CreateBatteryInfo() => new SDL2BatteryInfo();
|
||||
|
||||
private readonly List<string> importableFiles = new List<string>();
|
||||
private ScheduledDelegate? importSchedule;
|
||||
|
||||
private void fileDrop(string[] filePaths)
|
||||
{
|
||||
lock (importableFiles)
|
||||
{
|
||||
importableFiles.AddRange(filePaths);
|
||||
|
||||
Logger.Log($"Adding {filePaths.Length} files for import");
|
||||
|
||||
// File drag drop operations can potentially trigger hundreds or thousands of these calls on some platforms.
|
||||
// In order to avoid spawning multiple import tasks for a single drop operation, debounce a touch.
|
||||
importSchedule?.Cancel();
|
||||
importSchedule = Scheduler.AddDelayed(handlePendingImports, 100);
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePendingImports()
|
||||
{
|
||||
lock (importableFiles)
|
||||
{
|
||||
Logger.Log($"Handling batch import of {importableFiles.Count} files");
|
||||
|
||||
string[] paths = importableFiles.ToArray();
|
||||
importableFiles.Clear();
|
||||
|
||||
Task.Factory.StartNew(() => Import(paths), TaskCreationOptions.LongRunning);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
// 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.Allocation;
|
||||
using osu.Framework.Caching;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
/// <summary>
|
||||
/// A grid which displays coloured beat divisor lines in proximity to the selection or placement cursor.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This class heavily borrows from osu!mania's implementation (ManiaBeatSnapGrid).
|
||||
/// If further changes are to be made, they should also be applied there.
|
||||
/// If the scale of the changes are large enough, abstracting may be a good path.
|
||||
/// </remarks>
|
||||
public partial class CatchBeatSnapGrid : Component
|
||||
{
|
||||
private const double visible_range = 750;
|
||||
|
||||
/// <summary>
|
||||
/// The range of time values of the current selection.
|
||||
/// </summary>
|
||||
public (double start, double end)? SelectionTimeRange
|
||||
{
|
||||
set
|
||||
{
|
||||
if (value == selectionTimeRange)
|
||||
return;
|
||||
|
||||
selectionTimeRange = value;
|
||||
lineCache.Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private EditorBeatmap beatmap { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private BindableBeatDivisor beatDivisor { get; set; } = null!;
|
||||
|
||||
private readonly Cached lineCache = new Cached();
|
||||
|
||||
private (double start, double end)? selectionTimeRange;
|
||||
|
||||
private ScrollingHitObjectContainer lineContainer = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(HitObjectComposer composer)
|
||||
{
|
||||
lineContainer = new ScrollingHitObjectContainer();
|
||||
|
||||
((CatchPlayfield)composer.Playfield).UnderlayElements.Add(lineContainer);
|
||||
|
||||
beatDivisor.BindValueChanged(_ => createLines(), true);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (!lineCache.IsValid)
|
||||
{
|
||||
lineCache.Validate();
|
||||
createLines();
|
||||
}
|
||||
}
|
||||
|
||||
private readonly Stack<DrawableGridLine> availableLines = new Stack<DrawableGridLine>();
|
||||
|
||||
private void createLines()
|
||||
{
|
||||
foreach (var line in lineContainer.Objects.OfType<DrawableGridLine>())
|
||||
availableLines.Push(line);
|
||||
|
||||
lineContainer.Clear();
|
||||
|
||||
if (selectionTimeRange == null)
|
||||
return;
|
||||
|
||||
var range = selectionTimeRange.Value;
|
||||
|
||||
var timingPoint = beatmap.ControlPointInfo.TimingPointAt(range.start - visible_range);
|
||||
|
||||
double time = timingPoint.Time;
|
||||
int beat = 0;
|
||||
|
||||
// progress time until in the visible range.
|
||||
while (time < range.start - visible_range)
|
||||
{
|
||||
time += timingPoint.BeatLength / beatDivisor.Value;
|
||||
beat++;
|
||||
}
|
||||
|
||||
while (time < range.end + visible_range)
|
||||
{
|
||||
var nextTimingPoint = beatmap.ControlPointInfo.TimingPointAt(time);
|
||||
|
||||
// switch to the next timing point if we have reached it.
|
||||
if (nextTimingPoint.Time > timingPoint.Time)
|
||||
{
|
||||
beat = 0;
|
||||
time = nextTimingPoint.Time;
|
||||
timingPoint = nextTimingPoint;
|
||||
}
|
||||
|
||||
Color4 colour = BindableBeatDivisor.GetColourFor(
|
||||
BindableBeatDivisor.GetDivisorForBeatIndex(beat, beatDivisor.Value), colours);
|
||||
|
||||
if (!availableLines.TryPop(out var line))
|
||||
line = new DrawableGridLine();
|
||||
|
||||
line.HitObject.StartTime = time;
|
||||
line.Colour = colour;
|
||||
|
||||
lineContainer.Add(line);
|
||||
|
||||
beat++;
|
||||
time += timingPoint.BeatLength / beatDivisor.Value;
|
||||
}
|
||||
|
||||
// required to update ScrollingHitObjectContainer's cache.
|
||||
lineContainer.UpdateSubTree();
|
||||
|
||||
foreach (var line in lineContainer.Objects.OfType<DrawableGridLine>())
|
||||
{
|
||||
time = line.HitObject.StartTime;
|
||||
|
||||
if (time >= range.start && time <= range.end)
|
||||
line.Alpha = 1;
|
||||
else
|
||||
{
|
||||
double timeSeparation = time < range.start ? range.start - time : time - range.end;
|
||||
line.Alpha = (float)Math.Max(0, 1 - timeSeparation / visible_range);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private partial class DrawableGridLine : DrawableHitObject
|
||||
{
|
||||
public DrawableGridLine()
|
||||
: base(new HitObject())
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Height = 2;
|
||||
|
||||
AddInternal(new Box { RelativeSizeAxes = Axes.Both });
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Origin = Anchor.BottomLeft;
|
||||
Anchor = Anchor.BottomLeft;
|
||||
}
|
||||
|
||||
protected override void UpdateInitialTransforms()
|
||||
{
|
||||
// don't perform any fading – we are handling that ourselves.
|
||||
LifetimeEnd = HitObject.StartTime + visible_range;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,8 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
|
||||
private InputManager inputManager = null!;
|
||||
|
||||
private CatchBeatSnapGrid beatSnapGrid = null!;
|
||||
|
||||
private readonly BindableDouble timeRangeMultiplier = new BindableDouble(1)
|
||||
{
|
||||
MinValue = 1,
|
||||
@@ -65,6 +67,8 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
Catcher.BASE_DASH_SPEED, -Catcher.BASE_DASH_SPEED,
|
||||
Catcher.BASE_WALK_SPEED, -Catcher.BASE_WALK_SPEED,
|
||||
}));
|
||||
|
||||
AddInternal(beatSnapGrid = new CatchBeatSnapGrid());
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@@ -74,6 +78,29 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
inputManager = GetContainingInputManager();
|
||||
}
|
||||
|
||||
protected override void UpdateAfterChildren()
|
||||
{
|
||||
base.UpdateAfterChildren();
|
||||
|
||||
if (BlueprintContainer.CurrentTool is SelectTool)
|
||||
{
|
||||
if (EditorBeatmap.SelectedHitObjects.Any())
|
||||
{
|
||||
beatSnapGrid.SelectionTimeRange = (EditorBeatmap.SelectedHitObjects.Min(h => h.StartTime), EditorBeatmap.SelectedHitObjects.Max(h => h.GetEndTime()));
|
||||
}
|
||||
else
|
||||
beatSnapGrid.SelectionTimeRange = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
var result = FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position);
|
||||
if (result.Time is double time)
|
||||
beatSnapGrid.SelectionTimeRange = (time, time);
|
||||
else
|
||||
beatSnapGrid.SelectionTimeRange = null;
|
||||
}
|
||||
}
|
||||
|
||||
protected override double ReadCurrentDistanceSnap(HitObject before, HitObject after)
|
||||
{
|
||||
// osu!catch's distance snap implementation is limited, in that a custom spacing cannot be specified.
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.Objects.Drawables;
|
||||
@@ -41,6 +42,8 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
|
||||
internal CatcherArea CatcherArea { get; private set; } = null!;
|
||||
|
||||
public Container UnderlayElements { get; private set; } = null!;
|
||||
|
||||
private readonly IBeatmapDifficultyInfo difficulty;
|
||||
|
||||
public CatchPlayfield(IBeatmapDifficultyInfo difficulty)
|
||||
@@ -62,6 +65,10 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
|
||||
AddRangeInternal(new[]
|
||||
{
|
||||
UnderlayElements = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
droppedObjectContainer,
|
||||
Catcher.CreateProxiedContent(),
|
||||
HitObjectContainer.CreateProxy(),
|
||||
|
||||
@@ -8,6 +8,7 @@ using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
||||
{
|
||||
@@ -25,22 +26,35 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
||||
new StageDefinition(2)
|
||||
};
|
||||
|
||||
SetContents(_ => new ManiaPlayfield(stageDefinitions));
|
||||
SetContents(_ => new ManiaInputManager(new ManiaRuleset().RulesetInfo, 2)
|
||||
{
|
||||
Child = new ManiaPlayfield(stageDefinitions)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDualStages()
|
||||
[TestCase(2)]
|
||||
[TestCase(3)]
|
||||
[TestCase(5)]
|
||||
public void TestDualStages(int columnCount)
|
||||
{
|
||||
AddStep("create stage", () =>
|
||||
{
|
||||
stageDefinitions = new List<StageDefinition>
|
||||
{
|
||||
new StageDefinition(2),
|
||||
new StageDefinition(2)
|
||||
new StageDefinition(columnCount),
|
||||
new StageDefinition(columnCount)
|
||||
};
|
||||
|
||||
SetContents(_ => new ManiaPlayfield(stageDefinitions));
|
||||
SetContents(_ => new ManiaInputManager(new ManiaRuleset().RulesetInfo, (int)PlayfieldType.Dual + 2 * columnCount)
|
||||
{
|
||||
Child = new ManiaPlayfield(stageDefinitions)
|
||||
{
|
||||
// bit of a hack to make sure the dual stages fit on screen without overlapping each other.
|
||||
Size = new Vector2(1.5f),
|
||||
Scale = new Vector2(1 / 1.5f)
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -119,14 +119,12 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
yield return obj;
|
||||
}
|
||||
|
||||
private readonly List<double> prevNoteTimes = new List<double>(max_notes_for_density);
|
||||
private readonly LimitedCapacityQueue<double> prevNoteTimes = new LimitedCapacityQueue<double>(max_notes_for_density);
|
||||
private double density = int.MaxValue;
|
||||
|
||||
private void computeDensity(double newNoteTime)
|
||||
{
|
||||
if (prevNoteTimes.Count == max_notes_for_density)
|
||||
prevNoteTimes.RemoveAt(0);
|
||||
prevNoteTimes.Add(newNoteTime);
|
||||
prevNoteTimes.Enqueue(newNoteTime);
|
||||
|
||||
if (prevNoteTimes.Count >= 2)
|
||||
density = (prevNoteTimes[^1] - prevNoteTimes[0]) / prevNoteTimes.Count;
|
||||
|
||||
@@ -129,7 +129,7 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
}
|
||||
|
||||
Color4 colour = BindableBeatDivisor.GetColourFor(
|
||||
BindableBeatDivisor.GetDivisorForBeatIndex(Math.Max(1, beat), beatDivisor.Value), colours);
|
||||
BindableBeatDivisor.GetDivisorForBeatIndex(beat, beatDivisor.Value), colours);
|
||||
|
||||
foreach (var grid in grids)
|
||||
{
|
||||
|
||||
@@ -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.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
|
||||
|
||||
@@ -242,18 +242,23 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
bodyPiece.Y = (Direction.Value == ScrollingDirection.Up ? 1 : -1) * Head.Height / 2;
|
||||
bodyPiece.Height = DrawHeight - Head.Height / 2 + Tail.Height / 2;
|
||||
|
||||
// As the note is being held, adjust the size of the sizing container. This has two effects:
|
||||
// 1. The contained masking container will mask the body and ticks.
|
||||
// 2. The head note will move along with the new "head position" in the container.
|
||||
//
|
||||
// As per stable, this should not apply for early hits, waiting until the object starts to touch the
|
||||
// judgement area first.
|
||||
if (Head.IsHit && releaseTime == null && DrawHeight > 0 && Time.Current >= HitObject.StartTime)
|
||||
if (Time.Current >= HitObject.StartTime)
|
||||
{
|
||||
// How far past the hit target this hold note is.
|
||||
float yOffset = Direction.Value == ScrollingDirection.Up ? -Y : Y;
|
||||
sizingContainer.Height = 1 - yOffset / DrawHeight;
|
||||
// As the note is being held, adjust the size of the sizing container. This has two effects:
|
||||
// 1. The contained masking container will mask the body and ticks.
|
||||
// 2. The head note will move along with the new "head position" in the container.
|
||||
//
|
||||
// As per stable, this should not apply for early hits, waiting until the object starts to touch the
|
||||
// judgement area first.
|
||||
if (Head.IsHit && releaseTime == null && DrawHeight > 0)
|
||||
{
|
||||
// How far past the hit target this hold note is.
|
||||
float yOffset = Direction.Value == ScrollingDirection.Up ? -Y : Y;
|
||||
sizingContainer.Height = 1 - yOffset / DrawHeight;
|
||||
}
|
||||
}
|
||||
else
|
||||
sizingContainer.Height = 1;
|
||||
}
|
||||
|
||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||
|
||||
@@ -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
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Objects
|
||||
{
|
||||
public class HeadNote : Note
|
||||
|
||||
@@ -139,11 +139,11 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
case 3:
|
||||
switch (columnIndex)
|
||||
{
|
||||
case 0: return colour_pink;
|
||||
case 0: return colour_green;
|
||||
|
||||
case 1: return colour_orange;
|
||||
case 1: return colour_special_column;
|
||||
|
||||
case 2: return colour_yellow;
|
||||
case 2: return colour_cyan;
|
||||
|
||||
default: throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
@@ -185,11 +185,11 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
|
||||
case 1: return colour_orange;
|
||||
|
||||
case 2: return colour_yellow;
|
||||
case 2: return colour_green;
|
||||
|
||||
case 3: return colour_cyan;
|
||||
|
||||
case 4: return colour_purple;
|
||||
case 4: return colour_orange;
|
||||
|
||||
case 5: return colour_pink;
|
||||
|
||||
@@ -201,17 +201,17 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
{
|
||||
case 0: return colour_pink;
|
||||
|
||||
case 1: return colour_cyan;
|
||||
case 1: return colour_orange;
|
||||
|
||||
case 2: return colour_pink;
|
||||
|
||||
case 3: return colour_special_column;
|
||||
|
||||
case 4: return colour_green;
|
||||
case 4: return colour_pink;
|
||||
|
||||
case 5: return colour_cyan;
|
||||
case 5: return colour_orange;
|
||||
|
||||
case 6: return colour_green;
|
||||
case 6: return colour_pink;
|
||||
|
||||
default: throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
@@ -225,9 +225,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
|
||||
case 2: return colour_orange;
|
||||
|
||||
case 3: return colour_yellow;
|
||||
case 3: return colour_green;
|
||||
|
||||
case 4: return colour_yellow;
|
||||
case 4: return colour_cyan;
|
||||
|
||||
case 5: return colour_orange;
|
||||
|
||||
@@ -273,9 +273,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
|
||||
case 3: return colour_yellow;
|
||||
|
||||
case 4: return colour_cyan;
|
||||
case 4: return colour_green;
|
||||
|
||||
case 5: return colour_green;
|
||||
case 5: return colour_cyan;
|
||||
|
||||
case 6: return colour_yellow;
|
||||
|
||||
|
||||
@@ -35,10 +35,12 @@ namespace osu.Game.Rulesets.Mania.Skinning.Default
|
||||
|
||||
var stage = beatmap.GetStageForColumnIndex(column);
|
||||
|
||||
if (stage.IsSpecialColumn(column))
|
||||
int columnInStage = column % stage.Columns;
|
||||
|
||||
if (stage.IsSpecialColumn(columnInStage))
|
||||
return SkinUtils.As<TValue>(new Bindable<Color4>(colourSpecial));
|
||||
|
||||
int distanceToEdge = Math.Min(column, (stage.Columns - 1) - column);
|
||||
int distanceToEdge = Math.Min(columnInStage, (stage.Columns - 1) - columnInStage);
|
||||
return SkinUtils.As<TValue>(new Bindable<Color4>(distanceToEdge % 2 == 0 ? colourOdd : colourEven));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ using osu.Framework.Graphics.Pooling;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Objects.Drawables;
|
||||
@@ -39,7 +40,11 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
public readonly Bindable<ManiaAction> Action = new Bindable<ManiaAction>();
|
||||
|
||||
public readonly ColumnHitObjectArea HitObjectArea;
|
||||
|
||||
internal readonly Container BackgroundContainer = new Container { RelativeSizeAxes = Axes.Both };
|
||||
|
||||
internal readonly Container TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both };
|
||||
|
||||
private DrawablePool<PoolableHitExplosion> hitExplosionPool;
|
||||
private readonly OrderedHitPolicy hitPolicy;
|
||||
public Container UnderlayElements => HitObjectArea.UnderlayElements;
|
||||
@@ -76,30 +81,31 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
skin.SourceChanged += onSourceChanged;
|
||||
onSourceChanged();
|
||||
|
||||
Drawable background = new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.ColumnBackground), _ => new DefaultColumnBackground())
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
};
|
||||
|
||||
InternalChildren = new[]
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
hitExplosionPool = new DrawablePool<PoolableHitExplosion>(5),
|
||||
sampleTriggerSource = new GameplaySampleTriggerSource(HitObjectContainer),
|
||||
// For input purposes, the background is added at the highest depth, but is then proxied back below all other elements
|
||||
background.CreateProxy(),
|
||||
HitObjectArea,
|
||||
keyArea = new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.KeyArea), _ => new DefaultKeyArea())
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
background,
|
||||
// For input purposes, the background is added at the highest depth, but is then proxied back below all other elements externally
|
||||
// (see `Stage.columnBackgrounds`).
|
||||
BackgroundContainer,
|
||||
TopLevelContainer,
|
||||
new ColumnTouchInputArea(this)
|
||||
};
|
||||
|
||||
applyGameWideClock(background);
|
||||
applyGameWideClock(keyArea);
|
||||
var background = new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.ColumnBackground), _ => new DefaultColumnBackground())
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
};
|
||||
|
||||
background.ApplyGameWideClock(host);
|
||||
keyArea.ApplyGameWideClock(host);
|
||||
|
||||
BackgroundContainer.Add(background);
|
||||
TopLevelContainer.Add(HitObjectArea.Explosions.CreateProxy());
|
||||
|
||||
RegisterPool<Note, DrawableNote>(10, 50);
|
||||
@@ -107,18 +113,6 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
RegisterPool<HeadNote, DrawableHoldNoteHead>(10, 50);
|
||||
RegisterPool<TailNote, DrawableHoldNoteTail>(10, 50);
|
||||
RegisterPool<HoldNoteTick, DrawableHoldNoteTick>(50, 250);
|
||||
|
||||
// Some elements don't handle rewind correctly and fixing them is non-trivial.
|
||||
// In the future we need a better solution to this, but as a temporary work-around, give these components the game-wide
|
||||
// clock so they don't need to worry about rewind.
|
||||
// This only works because they handle OnPressed/OnReleased which results in a correct state while rewinding.
|
||||
//
|
||||
// This is kinda dodgy (and will cause weirdness when pausing gameplay) but is better than completely broken rewind.
|
||||
void applyGameWideClock(Drawable drawable)
|
||||
{
|
||||
drawable.Clock = host.UpdateThread.Clock;
|
||||
drawable.ProcessCustomClock = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void onSourceChanged()
|
||||
|
||||
@@ -60,6 +60,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
RelativeSizeAxes = Axes.Y;
|
||||
AutoSizeAxes = Axes.X;
|
||||
|
||||
Container columnBackgrounds;
|
||||
Container topLevelContainer;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
@@ -77,9 +78,10 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
},
|
||||
columnFlow = new ColumnFlow<Column>(definition)
|
||||
columnBackgrounds = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Name = "Column backgrounds",
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new Container
|
||||
{
|
||||
@@ -98,6 +100,10 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
}
|
||||
},
|
||||
columnFlow = new ColumnFlow<Column>(definition)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
},
|
||||
new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.StageForeground), _ => null)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
@@ -126,6 +132,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
};
|
||||
|
||||
topLevelContainer.Add(column.TopLevelContainer.CreateProxy());
|
||||
columnBackgrounds.Add(column.BackgroundContainer.CreateProxy());
|
||||
columnFlow.SetContentForColumn(i, column);
|
||||
AddNested(column);
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
|
||||
AddStep("place first object", () => InputManager.Click(MouseButton.Left));
|
||||
|
||||
AddStep("move mouse slightly", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.02f, 0)));
|
||||
AddStep("move mouse slightly", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.01f, 0)));
|
||||
|
||||
AddStep("place second object", () => InputManager.Click(MouseButton.Left));
|
||||
|
||||
@@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
|
||||
AddStep("enter circle placement mode", () => InputManager.Key(Key.Number2));
|
||||
|
||||
AddStep("move mouse slightly", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.235f, 0)));
|
||||
AddStep("move mouse slightly", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.205f, 0)));
|
||||
|
||||
AddStep("place second object", () => InputManager.Click(MouseButton.Left));
|
||||
|
||||
@@ -122,7 +122,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
|
||||
AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left));
|
||||
|
||||
AddStep("move mouse slightly off centre", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.02f, 0)));
|
||||
AddStep("move mouse slightly off centre", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.01f, 0)));
|
||||
|
||||
AddAssert("object 3 snapped to 1", () =>
|
||||
{
|
||||
@@ -134,7 +134,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
return Precision.AlmostEquals(first.EndPosition, third.Position);
|
||||
});
|
||||
|
||||
AddStep("move mouse slightly off centre", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * -0.22f, playfield.ScreenSpaceDrawQuad.Width * 0.21f)));
|
||||
AddStep("move mouse slightly off centre", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * -0.21f, playfield.ScreenSpaceDrawQuad.Width * 0.205f)));
|
||||
|
||||
AddAssert("object 2 snapped to 1", () =>
|
||||
{
|
||||
|
||||
@@ -185,7 +185,18 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
|
||||
AddAssert("Ensure cursor is on a grid line", () =>
|
||||
{
|
||||
return grid.ChildrenOfType<CircularProgress>().Any(p => Precision.AlmostEquals(p.ScreenSpaceDrawQuad.TopRight.X, grid.ToScreenSpace(cursor.LastSnappedPosition).X));
|
||||
return grid.ChildrenOfType<CircularProgress>().Any(ring =>
|
||||
{
|
||||
// the grid rings are actually slightly _larger_ than the snapping radii.
|
||||
// this is done such that the snapping radius falls right in the middle of each grid ring thickness-wise,
|
||||
// but it does however complicate the following calculations slightly.
|
||||
|
||||
// we want to calculate the coordinates of the rightmost point on the grid line, which is in the exact middle of the ring thickness-wise.
|
||||
// for the X component, we take the entire width of the ring, minus one half of the inner radius (since we want the middle of the line on the right side).
|
||||
// for the Y component, we just take 0.5f.
|
||||
var rightMiddleOfGridLine = ring.ToScreenSpace(ring.DrawSize * new Vector2(1 - ring.InnerRadius / 2, 0.5f));
|
||||
return Precision.AlmostEquals(rightMiddleOfGridLine.X, grid.ToScreenSpace(cursor.LastSnappedPosition).X);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,29 @@
|
||||
// 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.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Skinning.Default;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
{
|
||||
public partial class TestSceneOsuModAutoplay : OsuModTestScene
|
||||
{
|
||||
protected override bool AllowFail => true;
|
||||
|
||||
[Test]
|
||||
public void TestCursorPositionStoredToJudgement()
|
||||
{
|
||||
@@ -44,6 +51,36 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
FinalRate = { Value = 1.3 }
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestPerfectScoreOnShortSliderWithRepeat()
|
||||
{
|
||||
AddStep("set score to standardised", () => LocalConfig.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised));
|
||||
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Autoplay = true,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Slider
|
||||
{
|
||||
StartTime = 500,
|
||||
Position = new Vector2(256, 192),
|
||||
Path = new SliderPath(new[]
|
||||
{
|
||||
new PathControlPoint(),
|
||||
new PathControlPoint(new Vector2(0, 6.25f))
|
||||
}),
|
||||
RepeatCount = 1,
|
||||
SliderVelocity = 10
|
||||
}
|
||||
}
|
||||
},
|
||||
PassCondition = () => Player.ScoreProcessor.TotalScore.Value == 1_000_000
|
||||
});
|
||||
}
|
||||
|
||||
private void runSpmTest(Mod mod)
|
||||
{
|
||||
SpinnerSpmCalculator? spmCalculator = 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
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
{
|
||||
public enum SliderPosition
|
||||
|
||||
@@ -187,7 +187,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition);
|
||||
|
||||
float snapRadius =
|
||||
playfield.GamefieldToScreenSpace(new Vector2(OsuHitObject.OBJECT_RADIUS / 5)).X -
|
||||
playfield.GamefieldToScreenSpace(new Vector2(OsuHitObject.OBJECT_RADIUS * 0.10f)).X -
|
||||
playfield.GamefieldToScreenSpace(Vector2.Zero).X;
|
||||
|
||||
foreach (var b in blueprints)
|
||||
|
||||
@@ -18,7 +18,7 @@ using static osu.Game.Input.Handlers.ReplayInputHandler;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToPlayer
|
||||
public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToPlayer, IHasNoTimedInputs
|
||||
{
|
||||
public override LocalisableString Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things.";
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
// 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.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
/// <summary>
|
||||
/// Mod that colours <see cref="HitObject"/>s based on the musical division they are on
|
||||
/// </summary>
|
||||
public class OsuModSynesthesia : ModSynesthesia, IApplicableToBeatmap, IApplicableToDrawableHitObject
|
||||
{
|
||||
private readonly OsuColour colours = new OsuColour();
|
||||
|
||||
private IBeatmap? currentBeatmap { get; set; }
|
||||
|
||||
public void ApplyToBeatmap(IBeatmap beatmap)
|
||||
{
|
||||
//Store a reference to the current beatmap to look up the beat divisor when notes are drawn
|
||||
if (currentBeatmap != beatmap)
|
||||
currentBeatmap = beatmap;
|
||||
}
|
||||
|
||||
public void ApplyToDrawableHitObject(DrawableHitObject d)
|
||||
{
|
||||
if (currentBeatmap == null) return;
|
||||
|
||||
Color4? timingBasedColour = null;
|
||||
|
||||
d.HitObjectApplied += _ =>
|
||||
{
|
||||
// slider tails are a painful edge case, as their start time is offset 36ms back (see `LegacyLastTick`).
|
||||
// to work around this, look up the slider tail's parenting slider's end time instead to ensure proper snap.
|
||||
double snapTime = d is DrawableSliderTail tail
|
||||
? tail.Slider.GetEndTime()
|
||||
: d.HitObject.StartTime;
|
||||
timingBasedColour = BindableBeatDivisor.GetColourFor(currentBeatmap.ControlPointInfo.GetClosestBeatDivisor(snapTime), colours);
|
||||
};
|
||||
|
||||
// Need to set this every update to ensure it doesn't get overwritten by DrawableHitObject.OnApply() -> UpdateComboColour().
|
||||
d.OnUpdate += _ =>
|
||||
{
|
||||
if (timingBasedColour != null)
|
||||
d.AccentColour.Value = timingBasedColour.Value;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,18 +75,24 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
tailContainer = new Container<DrawableSliderTail> { RelativeSizeAxes = Axes.Both };
|
||||
|
||||
AddRangeInternal(new Drawable[]
|
||||
{
|
||||
shakeContainer = new ShakeContainer
|
||||
{
|
||||
ShakeDuration = 30,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
Children = new[]
|
||||
{
|
||||
Body = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.SliderBody), _ => new DefaultSliderBody(), confineMode: ConfineMode.NoScaling),
|
||||
tailContainer = new Container<DrawableSliderTail> { RelativeSizeAxes = Axes.Both },
|
||||
// proxied here so that the tail is drawn under repeats/ticks - legacy skins rely on this
|
||||
tailContainer.CreateProxy(),
|
||||
tickContainer = new Container<DrawableSliderTick> { RelativeSizeAxes = Axes.Both },
|
||||
repeatContainer = new Container<DrawableSliderRepeat> { RelativeSizeAxes = Axes.Both },
|
||||
// actual tail container is placed here to ensure that tail hitobjects are processed after ticks/repeats.
|
||||
// this is required for the correct operation of Score V2.
|
||||
tailContainer,
|
||||
}
|
||||
},
|
||||
// slider head is not included in shake as it handles hit detection, and handles its own shaking.
|
||||
|
||||
@@ -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
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
public interface IRequireTracking
|
||||
|
||||
@@ -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
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects
|
||||
{
|
||||
public interface ISliderProgress
|
||||
|
||||
@@ -204,7 +204,8 @@ namespace osu.Game.Rulesets.Osu
|
||||
new MultiMod(new OsuModMagnetised(), new OsuModRepel()),
|
||||
new ModAdaptiveSpeed(),
|
||||
new OsuModFreezeFrame(),
|
||||
new OsuModBubbles()
|
||||
new OsuModBubbles(),
|
||||
new OsuModSynesthesia()
|
||||
};
|
||||
|
||||
case ModType.System:
|
||||
|
||||
@@ -48,21 +48,26 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
|
||||
private Bindable<bool> configHitLighting = null!;
|
||||
|
||||
private static readonly Vector2 circle_size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
|
||||
|
||||
[Resolved]
|
||||
private DrawableHitObject drawableObject { get; set; } = null!;
|
||||
|
||||
public ArgonMainCirclePiece(bool withOuterFill)
|
||||
{
|
||||
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
|
||||
Size = circle_size;
|
||||
|
||||
Anchor = Anchor.Centre;
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
outerFill = new Circle // renders white outer border and dark fill
|
||||
outerFill = new Circle // renders dark fill
|
||||
{
|
||||
Size = Size,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
// Slightly inset to prevent bleeding outside the ring
|
||||
Size = circle_size - new Vector2(1),
|
||||
Alpha = withOuterFill ? 1 : 0,
|
||||
},
|
||||
outerGradient = new Circle // renders the outer bright gradient
|
||||
@@ -88,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
Masking = true,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = Size,
|
||||
Size = circle_size,
|
||||
Child = new KiaiFlash
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
|
||||
@@ -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
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Objects
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
|
||||
|
||||
private const double pre_beat_transition_time = 80;
|
||||
|
||||
private const float flash_opacity = 0.3f;
|
||||
private const float kiai_flash_opacity = 0.15f;
|
||||
|
||||
private ColourInfo accentColour;
|
||||
|
||||
@@ -107,7 +107,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
|
||||
if (drawableHitObject.State.Value == ArmedState.Idle)
|
||||
{
|
||||
flash
|
||||
.FadeTo(flash_opacity)
|
||||
.FadeTo(kiai_flash_opacity)
|
||||
.Then()
|
||||
.FadeOut(timingPoint.BeatLength * 0.75, Easing.OutSine);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
// 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.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
@@ -18,7 +16,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
|
||||
public partial class ArgonHitExplosion : CompositeDrawable, IAnimatableHitExplosion
|
||||
{
|
||||
private readonly TaikoSkinComponents component;
|
||||
|
||||
private readonly Circle outer;
|
||||
private readonly Circle inner;
|
||||
|
||||
public ArgonHitExplosion(TaikoSkinComponents component)
|
||||
{
|
||||
@@ -34,13 +34,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = ColourInfo.GradientVertical(
|
||||
new Color4(255, 227, 236, 255),
|
||||
new Color4(255, 198, 211, 255)
|
||||
),
|
||||
Masking = true,
|
||||
},
|
||||
new Circle
|
||||
inner = new Circle
|
||||
{
|
||||
Name = "Inner circle",
|
||||
Anchor = Anchor.Centre,
|
||||
@@ -48,12 +44,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.White,
|
||||
Size = new Vector2(0.85f),
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Glow,
|
||||
Colour = new Color4(255, 132, 191, 255).Opacity(0.5f),
|
||||
Radius = 45,
|
||||
},
|
||||
Masking = true,
|
||||
},
|
||||
};
|
||||
@@ -63,6 +53,16 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
|
||||
{
|
||||
this.FadeOut();
|
||||
|
||||
bool isRim = (drawableHitObject.HitObject as Hit)?.Type == HitType.Rim;
|
||||
|
||||
outer.Colour = isRim ? ArgonInputDrum.RIM_HIT_GRADIENT : ArgonInputDrum.CENTRE_HIT_GRADIENT;
|
||||
inner.EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Glow,
|
||||
Colour = (isRim ? ArgonInputDrum.RIM_HIT_GLOW : ArgonInputDrum.CENTRE_HIT_GLOW).Opacity(0.5f),
|
||||
Radius = 45,
|
||||
};
|
||||
|
||||
switch (component)
|
||||
{
|
||||
case TaikoSkinComponents.TaikoExplosionGreat:
|
||||
|
||||
@@ -19,6 +19,20 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
|
||||
{
|
||||
public partial class ArgonInputDrum : AspectContainer
|
||||
{
|
||||
public static readonly ColourInfo RIM_HIT_GRADIENT = ColourInfo.GradientHorizontal(
|
||||
new Color4(227, 248, 255, 255),
|
||||
new Color4(198, 245, 255, 255)
|
||||
);
|
||||
|
||||
public static readonly Colour4 RIM_HIT_GLOW = new Color4(126, 215, 253, 255);
|
||||
|
||||
public static readonly ColourInfo CENTRE_HIT_GRADIENT = ColourInfo.GradientHorizontal(
|
||||
new Color4(255, 227, 236, 255),
|
||||
new Color4(255, 198, 211, 255)
|
||||
);
|
||||
|
||||
public static readonly Colour4 CENTRE_HIT_GLOW = new Color4(255, 147, 199, 255);
|
||||
|
||||
private const float rim_size = 0.3f;
|
||||
|
||||
public ArgonInputDrum()
|
||||
@@ -141,14 +155,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
|
||||
Anchor = anchor,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = ColourInfo.GradientHorizontal(
|
||||
new Color4(227, 248, 255, 255),
|
||||
new Color4(198, 245, 255, 255)
|
||||
),
|
||||
Colour = RIM_HIT_GRADIENT,
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Glow,
|
||||
Colour = new Color4(126, 215, 253, 170),
|
||||
Colour = RIM_HIT_GLOW.Opacity(0.66f),
|
||||
Radius = 50,
|
||||
},
|
||||
Alpha = 0,
|
||||
@@ -166,14 +177,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
|
||||
Anchor = anchor,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = ColourInfo.GradientHorizontal(
|
||||
new Color4(255, 227, 236, 255),
|
||||
new Color4(255, 198, 211, 255)
|
||||
),
|
||||
Colour = CENTRE_HIT_GRADIENT,
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Glow,
|
||||
Colour = new Color4(255, 147, 199, 255),
|
||||
Colour = CENTRE_HIT_GLOW,
|
||||
Radius = 50,
|
||||
},
|
||||
Size = new Vector2(1 - rim_size),
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default
|
||||
|
||||
private const double pre_beat_transition_time = 80;
|
||||
|
||||
private const float flash_opacity = 0.3f;
|
||||
private const float kiai_flash_opacity = 0.15f;
|
||||
|
||||
[Resolved]
|
||||
private DrawableHitObject drawableHitObject { get; set; } = null!;
|
||||
@@ -187,7 +187,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default
|
||||
if (drawableHitObject.State.Value == ArmedState.Idle)
|
||||
{
|
||||
flashBox
|
||||
.FadeTo(flash_opacity)
|
||||
.FadeTo(kiai_flash_opacity)
|
||||
.Then()
|
||||
.FadeOut(timingPoint.BeatLength * 0.75, Easing.OutSine);
|
||||
}
|
||||
|
||||
@@ -231,7 +231,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
|
||||
protected override IBeatmap GetBeatmap() => beatmap;
|
||||
|
||||
protected override Texture GetBackground() => throw new NotImplementedException();
|
||||
public override Texture GetBackground() => throw new NotImplementedException();
|
||||
|
||||
protected override Track GetBeatmapTrack() => throw new NotImplementedException();
|
||||
|
||||
|
||||
@@ -131,7 +131,7 @@ namespace osu.Game.Tests.Editing.Checks
|
||||
|
||||
var mock = new Mock<IWorkingBeatmap>();
|
||||
mock.SetupGet(w => w.Beatmap).Returns(beatmap);
|
||||
mock.SetupGet(w => w.Background).Returns(background);
|
||||
mock.Setup(w => w.GetBackground()).Returns(background);
|
||||
mock.Setup(w => w.GetStream(It.IsAny<string>())).Returns(stream);
|
||||
|
||||
return mock;
|
||||
|
||||
@@ -73,7 +73,5 @@ namespace osu.Game.Tests.Rulesets
|
||||
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => null;
|
||||
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => null;
|
||||
}
|
||||
|
||||
#nullable enable
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,7 +286,7 @@ namespace osu.Game.Tests.Visual.Background
|
||||
this.renderer = renderer;
|
||||
}
|
||||
|
||||
protected override Texture GetBackground() => renderer.CreateTexture(1, 1);
|
||||
public override Texture GetBackground() => renderer.CreateTexture(1, 1);
|
||||
}
|
||||
|
||||
private partial class TestWorkingBeatmapWithStoryboard : TestWorkingBeatmap
|
||||
|
||||
@@ -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.Linq;
|
||||
@@ -23,8 +21,8 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
{
|
||||
public partial class TestSceneBeatDivisorControl : OsuManualInputManagerTestScene
|
||||
{
|
||||
private BeatDivisorControl beatDivisorControl;
|
||||
private BindableBeatDivisor bindableBeatDivisor;
|
||||
private BeatDivisorControl beatDivisorControl = null!;
|
||||
private BindableBeatDivisor bindableBeatDivisor = null!;
|
||||
|
||||
private SliderBar<int> tickSliderBar => beatDivisorControl.ChildrenOfType<SliderBar<int>>().Single();
|
||||
private Triangle tickMarkerHead => tickSliderBar.ChildrenOfType<Triangle>().Single();
|
||||
@@ -51,9 +49,9 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
[Test]
|
||||
public void TestBindableBeatDivisor()
|
||||
{
|
||||
AddRepeatStep("move previous", () => bindableBeatDivisor.Previous(), 2);
|
||||
AddRepeatStep("move previous", () => bindableBeatDivisor.SelectPrevious(), 2);
|
||||
AddAssert("divisor is 4", () => bindableBeatDivisor.Value == 4);
|
||||
AddRepeatStep("move next", () => bindableBeatDivisor.Next(), 1);
|
||||
AddRepeatStep("move next", () => bindableBeatDivisor.SelectNext(), 1);
|
||||
AddAssert("divisor is 12", () => bindableBeatDivisor.Value == 8);
|
||||
}
|
||||
|
||||
@@ -101,16 +99,22 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
public void TestBeatChevronNavigation()
|
||||
{
|
||||
switchBeatSnap(1);
|
||||
assertBeatSnap(16);
|
||||
|
||||
switchBeatSnap(-4);
|
||||
assertBeatSnap(1);
|
||||
|
||||
switchBeatSnap(3);
|
||||
assertBeatSnap(8);
|
||||
|
||||
switchBeatSnap(-1);
|
||||
switchBeatSnap(3);
|
||||
assertBeatSnap(16);
|
||||
|
||||
switchBeatSnap(-2);
|
||||
assertBeatSnap(4);
|
||||
|
||||
switchBeatSnap(-3);
|
||||
assertBeatSnap(16);
|
||||
assertBeatSnap(1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -163,9 +167,11 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
switchPresets(1);
|
||||
assertPreset(BeatDivisorType.Triplets);
|
||||
assertBeatSnap(6);
|
||||
|
||||
switchPresets(1);
|
||||
assertPreset(BeatDivisorType.Common);
|
||||
assertBeatSnap(4);
|
||||
|
||||
switchPresets(-1);
|
||||
assertPreset(BeatDivisorType.Triplets);
|
||||
@@ -181,6 +187,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
setDivisorViaInput(15);
|
||||
assertPreset(BeatDivisorType.Custom, 15);
|
||||
assertBeatSnap(15);
|
||||
|
||||
switchBeatSnap(-1);
|
||||
assertBeatSnap(5);
|
||||
@@ -190,12 +197,14 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
setDivisorViaInput(5);
|
||||
assertPreset(BeatDivisorType.Custom, 15);
|
||||
assertBeatSnap(5);
|
||||
|
||||
switchPresets(1);
|
||||
assertPreset(BeatDivisorType.Common);
|
||||
|
||||
switchPresets(-1);
|
||||
assertPreset(BeatDivisorType.Triplets);
|
||||
assertPreset(BeatDivisorType.Custom, 15);
|
||||
assertBeatSnap(15);
|
||||
}
|
||||
|
||||
private void switchBeatSnap(int direction) => AddRepeatStep($"move snap {(direction > 0 ? "forward" : "backward")}", () =>
|
||||
@@ -207,7 +216,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
}, Math.Abs(direction));
|
||||
|
||||
private void assertBeatSnap(int expected) => AddAssert($"beat snap is {expected}",
|
||||
() => bindableBeatDivisor.Value == expected);
|
||||
() => bindableBeatDivisor.Value, () => Is.EqualTo(expected));
|
||||
|
||||
private void switchPresets(int direction) => AddRepeatStep($"move presets {(direction > 0 ? "forward" : "backward")}", () =>
|
||||
{
|
||||
@@ -219,7 +228,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
private void assertPreset(BeatDivisorType type, int? maxDivisor = null)
|
||||
{
|
||||
AddAssert($"preset is {type}", () => bindableBeatDivisor.ValidDivisors.Value.Type == type);
|
||||
AddAssert($"preset is {type}", () => bindableBeatDivisor.ValidDivisors.Value.Type, () => Is.EqualTo(type));
|
||||
|
||||
if (type == BeatDivisorType.Custom)
|
||||
{
|
||||
@@ -237,7 +246,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
BeatDivisorControl.CustomDivisorPopover popover = null;
|
||||
BeatDivisorControl.CustomDivisorPopover? popover = null;
|
||||
AddUntilStep("wait for popover", () => (popover = this.ChildrenOfType<BeatDivisorControl.CustomDivisorPopover>().SingleOrDefault()) != null && popover.IsLoaded);
|
||||
AddStep($"set divisor to {divisor}", () =>
|
||||
{
|
||||
|
||||
@@ -8,6 +8,8 @@ using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play.PlayerSettings;
|
||||
using osu.Game.Tests.Visual.Ranking;
|
||||
@@ -49,6 +51,21 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddAssert("No calibration button", () => !offsetControl.ChildrenOfType<SettingsButton>().Any());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestModRemovingTimedInputs()
|
||||
{
|
||||
AddStep("Set score with mod removing timed inputs", () =>
|
||||
{
|
||||
offsetControl.ReferenceScore.Value = new ScoreInfo
|
||||
{
|
||||
HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(10),
|
||||
Mods = new Mod[] { new OsuModRelax() }
|
||||
};
|
||||
});
|
||||
|
||||
AddAssert("No calibration button", () => !offsetControl.ChildrenOfType<SettingsButton>().Any());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCalibrationFromZero()
|
||||
{
|
||||
|
||||
@@ -7,12 +7,15 @@ using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
@@ -21,6 +24,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
private GameplayClockContainer gameplayClockContainer = null!;
|
||||
|
||||
private Box background = null!;
|
||||
|
||||
private const double skip_target_time = -2000;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@@ -30,11 +35,20 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
FrameStabilityContainer frameStabilityContainer;
|
||||
|
||||
Add(gameplayClockContainer = new MasterGameplayClockContainer(Beatmap.Value, skip_target_time)
|
||||
AddRange(new Drawable[]
|
||||
{
|
||||
Child = frameStabilityContainer = new FrameStabilityContainer
|
||||
background = new Box
|
||||
{
|
||||
MaxCatchUpFrames = 1
|
||||
Colour = Color4.Black,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Depth = float.MaxValue
|
||||
},
|
||||
gameplayClockContainer = new MasterGameplayClockContainer(Beatmap.Value, skip_target_time)
|
||||
{
|
||||
Child = frameStabilityContainer = new FrameStabilityContainer
|
||||
{
|
||||
MaxCatchUpFrames = 1
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -71,9 +85,20 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
applyToArgonProgress(s => s.ShowGraph.Value = b);
|
||||
});
|
||||
|
||||
AddStep("set white background", () => background.FadeColour(Color4.White, 200, Easing.OutQuint));
|
||||
AddStep("randomise background colour", () => background.FadeColour(new Colour4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1), 200, Easing.OutQuint));
|
||||
|
||||
AddStep("stop", gameplayClockContainer.Stop);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSeekToKnownTime()
|
||||
{
|
||||
AddStep("seek to known time", () => gameplayClockContainer.Seek(60000));
|
||||
AddWaitStep("wait some for seek", 15);
|
||||
AddStep("stop", () => gameplayClockContainer.Stop());
|
||||
}
|
||||
|
||||
private void applyToArgonProgress(Action<ArgonSongProgress> action) =>
|
||||
this.ChildrenOfType<ArgonSongProgress>().ForEach(action);
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Testing;
|
||||
@@ -57,11 +58,36 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddAssert("customisation area not expanded", () => this.ChildrenOfType<ModSettingsArea>().Single().Height == 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSelectAllButtonUpdatesStateWhenSearchTermChanged()
|
||||
{
|
||||
createFreeModSelect();
|
||||
|
||||
AddStep("apply search term", () => freeModSelectOverlay.SearchTerm = "ea");
|
||||
|
||||
AddAssert("select all button enabled", () => this.ChildrenOfType<SelectAllModsButton>().Single().Enabled.Value);
|
||||
|
||||
AddStep("click select all button", navigateAndClick<SelectAllModsButton>);
|
||||
AddAssert("select all button disabled", () => !this.ChildrenOfType<SelectAllModsButton>().Single().Enabled.Value);
|
||||
|
||||
AddStep("change search term", () => freeModSelectOverlay.SearchTerm = "e");
|
||||
|
||||
AddAssert("select all button enabled", () => this.ChildrenOfType<SelectAllModsButton>().Single().Enabled.Value);
|
||||
|
||||
void navigateAndClick<T>() where T : Drawable
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<T>().Single());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSelectDeselectAllViaKeyboard()
|
||||
{
|
||||
createFreeModSelect();
|
||||
|
||||
AddStep("kill search bar focus", () => freeModSelectOverlay.SearchTextBox.KillFocus());
|
||||
|
||||
AddStep("press ctrl+a", () => InputManager.Keys(PlatformAction.SelectAll));
|
||||
AddUntilStep("all mods selected", assertAllAvailableModsSelected);
|
||||
|
||||
|
||||
@@ -94,6 +94,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
[TestCase(typeof(OsuModHidden), typeof(OsuModTraceable))] // Incompatible.
|
||||
public void TestAllowedModDeselectedWhenRequired(Type allowedMod, Type requiredMod)
|
||||
{
|
||||
AddStep("change ruleset", () => Ruleset.Value = new OsuRuleset().RulesetInfo);
|
||||
AddStep($"select {allowedMod.ReadableName()} as allowed", () => songSelect.FreeMods.Value = new[] { (Mod)Activator.CreateInstance(allowedMod) });
|
||||
AddStep($"select {requiredMod.ReadableName()} as required", () => songSelect.Mods.Value = new[] { (Mod)Activator.CreateInstance(requiredMod) });
|
||||
|
||||
@@ -102,17 +103,17 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
// A previous test's mod overlay could still be fading out.
|
||||
AddUntilStep("wait for only one freemod overlay", () => this.ChildrenOfType<FreeModSelectOverlay>().Count() == 1);
|
||||
|
||||
assertHasFreeModButton(allowedMod, false);
|
||||
assertHasFreeModButton(requiredMod, false);
|
||||
assertFreeModNotShown(allowedMod);
|
||||
assertFreeModNotShown(requiredMod);
|
||||
}
|
||||
|
||||
private void assertHasFreeModButton(Type type, bool hasButton = true)
|
||||
private void assertFreeModNotShown(Type type)
|
||||
{
|
||||
AddAssert($"{type.ReadableName()} {(hasButton ? "displayed" : "not displayed")} in freemod overlay",
|
||||
AddAssert($"{type.ReadableName()} not displayed in freemod overlay",
|
||||
() => this.ChildrenOfType<FreeModSelectOverlay>()
|
||||
.Single()
|
||||
.ChildrenOfType<ModPanel>()
|
||||
.Where(panel => !panel.Filtered.Value)
|
||||
.Where(panel => panel.Visible)
|
||||
.All(b => b.Mod.GetType() != type));
|
||||
}
|
||||
|
||||
|
||||
@@ -203,7 +203,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddUntilStep("mod select contains only double time mod",
|
||||
() => this.ChildrenOfType<RoomSubScreen>().Single().UserModsSelectOverlay
|
||||
.ChildrenOfType<ModPanel>()
|
||||
.SingleOrDefault(panel => !panel.Filtered.Value)?.Mod is OsuModDoubleTime);
|
||||
.SingleOrDefault(panel => panel.Visible)?.Mod is OsuModDoubleTime);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -17,6 +17,7 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Collections;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Leaderboards;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.BeatmapListing;
|
||||
@@ -539,6 +540,11 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
AddStep("open room", () => multiplayerComponents.ChildrenOfType<LoungeSubScreen>().Single().Open());
|
||||
AddStep("press back button", () => Game.ChildrenOfType<BackButton>().First().Action());
|
||||
AddWaitStep("wait two frames", 2);
|
||||
|
||||
AddStep("exit lounge", () => Game.ScreenStack.Exit());
|
||||
// `TestMultiplayerComponents` registers a request handler in its BDL, but never unregisters it.
|
||||
// to prevent the handler living for longer than it should be, clean up manually.
|
||||
AddStep("clean up multiplayer request handler", () => ((DummyAPIAccess)API).HandleRequest = null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -86,6 +86,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
StarRating = 9.99,
|
||||
DifficultyName = @"TEST",
|
||||
Length = 456000,
|
||||
HitLength = 400000,
|
||||
RulesetID = 3,
|
||||
CircleSize = 1,
|
||||
DrainRate = 2.3f,
|
||||
|
||||
@@ -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 System.Threading;
|
||||
@@ -24,8 +22,8 @@ namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
private readonly APIUser streamingUser = new APIUser { Id = 2, Username = "Test user" };
|
||||
|
||||
private TestSpectatorClient spectatorClient;
|
||||
private CurrentlyPlayingDisplay currentlyPlaying;
|
||||
private TestSpectatorClient spectatorClient = null!;
|
||||
private CurrentlyPlayingDisplay currentlyPlaying = null!;
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
@@ -88,13 +86,13 @@ namespace osu.Game.Tests.Visual.Online
|
||||
"pishifat"
|
||||
};
|
||||
|
||||
protected override Task<APIUser> ComputeValueAsync(int lookup, CancellationToken token = default)
|
||||
protected override Task<APIUser?> ComputeValueAsync(int lookup, CancellationToken token = default)
|
||||
{
|
||||
// tests against failed lookups
|
||||
if (lookup == 13)
|
||||
return Task.FromResult<APIUser>(null);
|
||||
return Task.FromResult<APIUser?>(null);
|
||||
|
||||
return Task.FromResult(new APIUser
|
||||
return Task.FromResult<APIUser?>(new APIUser
|
||||
{
|
||||
Id = lookup,
|
||||
Username = usernames[lookup % usernames.Length],
|
||||
|
||||
@@ -453,6 +453,25 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
AddStep("Un-filter", () => carousel.Filter(new FilterCriteria(), false));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRewindToDeletedBeatmap()
|
||||
{
|
||||
loadBeatmaps();
|
||||
|
||||
var firstAdded = TestResources.CreateTestBeatmapSetInfo();
|
||||
|
||||
AddStep("add new set", () => carousel.UpdateBeatmapSet(firstAdded));
|
||||
AddStep("select set", () => carousel.SelectBeatmap(firstAdded.Beatmaps.First()));
|
||||
|
||||
nextRandom();
|
||||
|
||||
AddStep("delete set", () => carousel.RemoveBeatmapSet(firstAdded));
|
||||
|
||||
prevRandom();
|
||||
|
||||
AddAssert("deleted set not selected", () => carousel.SelectedBeatmapSet?.Equals(firstAdded) == false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test adding and removing beatmap sets
|
||||
/// </summary>
|
||||
|
||||
@@ -1,10 +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.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -24,10 +23,10 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
public partial class TestSceneBeatmapMetadataDisplay : OsuTestScene
|
||||
{
|
||||
private BeatmapMetadataDisplay display;
|
||||
private BeatmapMetadataDisplay display = null!;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapManager manager { get; set; }
|
||||
private BeatmapManager manager { get; set; } = null!;
|
||||
|
||||
[Cached(typeof(BeatmapDifficultyCache))]
|
||||
private readonly TestBeatmapDifficultyCache testDifficultyCache = new TestBeatmapDifficultyCache();
|
||||
@@ -121,7 +120,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
private partial class TestBeatmapDifficultyCache : BeatmapDifficultyCache
|
||||
{
|
||||
private TaskCompletionSource<bool> calculationBlocker;
|
||||
private TaskCompletionSource<bool>? calculationBlocker;
|
||||
|
||||
private bool blockCalculation;
|
||||
|
||||
@@ -142,10 +141,13 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task<StarDifficulty?> GetDifficultyAsync(IBeatmapInfo beatmapInfo, IRulesetInfo rulesetInfo = null, IEnumerable<Mod> mods = null, CancellationToken cancellationToken = default)
|
||||
public override async Task<StarDifficulty?> GetDifficultyAsync(IBeatmapInfo beatmapInfo, IRulesetInfo? rulesetInfo = null, IEnumerable<Mod>? mods = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (blockCalculation)
|
||||
{
|
||||
Debug.Assert(calculationBlocker != null);
|
||||
await calculationBlocker.Task.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return await base.GetDifficultyAsync(beatmapInfo, rulesetInfo, mods, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -106,26 +106,26 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
});
|
||||
|
||||
AddStep("set filter", () => setFilter(mod => mod.Name.Contains("Wind", StringComparison.CurrentCultureIgnoreCase)));
|
||||
AddUntilStep("two panels visible", () => column.ChildrenOfType<ModPanel>().Count(panel => !panel.Filtered.Value) == 2);
|
||||
AddUntilStep("two panels visible", () => column.ChildrenOfType<ModPanel>().Count(panel => panel.Visible) == 2);
|
||||
|
||||
clickToggle();
|
||||
AddUntilStep("wait for animation", () => !column.SelectionAnimationRunning);
|
||||
AddAssert("only visible items selected", () => column.ChildrenOfType<ModPanel>().Where(panel => panel.Active.Value).All(panel => !panel.Filtered.Value));
|
||||
AddAssert("only visible items selected", () => column.ChildrenOfType<ModPanel>().Where(panel => panel.Active.Value).All(panel => panel.Visible));
|
||||
|
||||
AddStep("unset filter", () => setFilter(null));
|
||||
AddUntilStep("all panels visible", () => column.ChildrenOfType<ModPanel>().All(panel => !panel.Filtered.Value));
|
||||
AddUntilStep("all panels visible", () => column.ChildrenOfType<ModPanel>().All(panel => panel.Visible));
|
||||
AddAssert("checkbox not selected", () => !column.ChildrenOfType<OsuCheckbox>().Single().Current.Value);
|
||||
|
||||
AddStep("set filter", () => setFilter(mod => mod.Name.Contains("Wind", StringComparison.CurrentCultureIgnoreCase)));
|
||||
AddUntilStep("two panels visible", () => column.ChildrenOfType<ModPanel>().Count(panel => !panel.Filtered.Value) == 2);
|
||||
AddUntilStep("two panels visible", () => column.ChildrenOfType<ModPanel>().Count(panel => panel.Visible) == 2);
|
||||
AddAssert("checkbox selected", () => column.ChildrenOfType<OsuCheckbox>().Single().Current.Value);
|
||||
|
||||
AddStep("filter out everything", () => setFilter(_ => false));
|
||||
AddUntilStep("no panels visible", () => column.ChildrenOfType<ModPanel>().All(panel => panel.Filtered.Value));
|
||||
AddUntilStep("no panels visible", () => column.ChildrenOfType<ModPanel>().All(panel => !panel.Visible));
|
||||
AddUntilStep("checkbox hidden", () => !column.ChildrenOfType<OsuCheckbox>().Single().IsPresent);
|
||||
|
||||
AddStep("inset filter", () => setFilter(null));
|
||||
AddUntilStep("all panels visible", () => column.ChildrenOfType<ModPanel>().All(panel => !panel.Filtered.Value));
|
||||
AddUntilStep("all panels visible", () => column.ChildrenOfType<ModPanel>().All(panel => panel.Visible));
|
||||
AddUntilStep("checkbox visible", () => column.ChildrenOfType<OsuCheckbox>().Single().IsPresent);
|
||||
|
||||
void clickToggle() => AddStep("click toggle", () =>
|
||||
@@ -288,10 +288,53 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddAssert("no change", () => this.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value) == 2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestApplySearchTerms()
|
||||
{
|
||||
Mod hidden = getExampleModsFor(ModType.DifficultyIncrease).Where(modState => modState.Mod is ModHidden).Select(modState => modState.Mod).Single();
|
||||
|
||||
ModColumn column = null!;
|
||||
AddStep("create content", () => Child = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding(30),
|
||||
Child = column = new ModColumn(ModType.DifficultyIncrease, false)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
AvailableMods = getExampleModsFor(ModType.DifficultyIncrease)
|
||||
}
|
||||
});
|
||||
|
||||
applySearchAndAssert(hidden.Name);
|
||||
|
||||
clearSearch();
|
||||
|
||||
applySearchAndAssert(hidden.Acronym);
|
||||
|
||||
clearSearch();
|
||||
|
||||
applySearchAndAssert(hidden.Description.ToString());
|
||||
|
||||
void applySearchAndAssert(string searchTerm)
|
||||
{
|
||||
AddStep("search by mod name", () => column.SearchTerm = searchTerm);
|
||||
|
||||
AddAssert("only hidden is visible", () => column.ChildrenOfType<ModPanel>().Where(panel => panel.Visible).All(panel => panel.Mod is ModHidden));
|
||||
}
|
||||
|
||||
void clearSearch()
|
||||
{
|
||||
AddStep("clear search", () => column.SearchTerm = string.Empty);
|
||||
|
||||
AddAssert("all mods are visible", () => column.ChildrenOfType<ModPanel>().All(panel => panel.Visible));
|
||||
}
|
||||
}
|
||||
|
||||
private void setFilter(Func<Mod, bool>? filter)
|
||||
{
|
||||
foreach (var modState in this.ChildrenOfType<ModColumn>().Single().AvailableMods)
|
||||
modState.Filtered.Value = filter?.Invoke(modState.Mod) == false;
|
||||
modState.ValidForSelection.Value = filter?.Invoke(modState.Mod) != false;
|
||||
}
|
||||
|
||||
private partial class TestModColumn : ModColumn
|
||||
|
||||
@@ -392,6 +392,28 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
new HashSet<Mod>(this.ChildrenOfType<ModPresetPanel>().First().Preset.Value.Mods).SetEquals(mods));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTextFiltering()
|
||||
{
|
||||
ModPresetColumn modPresetColumn = null!;
|
||||
|
||||
AddStep("clear mods", () => SelectedMods.Value = Array.Empty<Mod>());
|
||||
AddStep("create content", () => Child = modPresetColumn = new ModPresetColumn
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
});
|
||||
|
||||
AddUntilStep("items loaded", () => modPresetColumn.IsLoaded && modPresetColumn.ItemsLoaded);
|
||||
|
||||
AddStep("set osu! ruleset", () => Ruleset.Value = rulesets.GetRuleset(0));
|
||||
AddStep("set text filter", () => modPresetColumn.SearchTerm = "First");
|
||||
AddUntilStep("one panel visible", () => modPresetColumn.ChildrenOfType<ModPresetPanel>().Count(panel => panel.IsPresent), () => Is.EqualTo(1));
|
||||
|
||||
AddStep("set mania ruleset", () => Ruleset.Value = rulesets.GetRuleset(3));
|
||||
AddUntilStep("no panels visible", () => modPresetColumn.ChildrenOfType<ModPresetPanel>().Count(panel => panel.IsPresent), () => Is.EqualTo(0));
|
||||
}
|
||||
|
||||
private ICollection<ModPreset> createTestPresets() => new[]
|
||||
{
|
||||
new ModPreset
|
||||
|
||||
@@ -490,15 +490,15 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
createScreen();
|
||||
changeRuleset(0);
|
||||
|
||||
AddAssert("double time visible", () => modSelectOverlay.ChildrenOfType<ModPanel>().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => !panel.Filtered.Value));
|
||||
AddAssert("double time visible", () => modSelectOverlay.ChildrenOfType<ModPanel>().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => panel.Visible));
|
||||
|
||||
AddStep("make double time invalid", () => modSelectOverlay.IsValidMod = m => !(m is OsuModDoubleTime));
|
||||
AddUntilStep("double time not visible", () => modSelectOverlay.ChildrenOfType<ModPanel>().Where(panel => panel.Mod is OsuModDoubleTime).All(panel => panel.Filtered.Value));
|
||||
AddAssert("nightcore still visible", () => modSelectOverlay.ChildrenOfType<ModPanel>().Where(panel => panel.Mod is OsuModNightcore).Any(panel => !panel.Filtered.Value));
|
||||
AddUntilStep("double time not visible", () => modSelectOverlay.ChildrenOfType<ModPanel>().Where(panel => panel.Mod is OsuModDoubleTime).All(panel => !panel.Visible));
|
||||
AddAssert("nightcore still visible", () => modSelectOverlay.ChildrenOfType<ModPanel>().Where(panel => panel.Mod is OsuModNightcore).Any(panel => panel.Visible));
|
||||
|
||||
AddStep("make double time valid again", () => modSelectOverlay.IsValidMod = _ => true);
|
||||
AddUntilStep("double time visible", () => modSelectOverlay.ChildrenOfType<ModPanel>().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => !panel.Filtered.Value));
|
||||
AddAssert("nightcore still visible", () => modSelectOverlay.ChildrenOfType<ModPanel>().Where(b => b.Mod is OsuModNightcore).Any(panel => !panel.Filtered.Value));
|
||||
AddUntilStep("double time visible", () => modSelectOverlay.ChildrenOfType<ModPanel>().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => panel.Visible));
|
||||
AddAssert("nightcore still visible", () => modSelectOverlay.ChildrenOfType<ModPanel>().Where(b => b.Mod is OsuModNightcore).Any(panel => panel.Visible));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -524,7 +524,57 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddStep("set ruleset", () => Ruleset.Value = testRuleset.RulesetInfo);
|
||||
waitForColumnLoad();
|
||||
|
||||
AddAssert("unimplemented mod panel is filtered", () => getPanelForMod(typeof(TestUnimplementedMod)).Filtered.Value);
|
||||
AddAssert("unimplemented mod panel is filtered", () => !getPanelForMod(typeof(TestUnimplementedMod)).Visible);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFirstModSelectDeselect()
|
||||
{
|
||||
createScreen();
|
||||
|
||||
AddStep("apply search", () => modSelectOverlay.SearchTerm = "HD");
|
||||
|
||||
AddStep("press enter", () => InputManager.Key(Key.Enter));
|
||||
AddAssert("hidden selected", () => getPanelForMod(typeof(OsuModHidden)).Active.Value);
|
||||
|
||||
AddStep("press enter again", () => InputManager.Key(Key.Enter));
|
||||
AddAssert("hidden deselected", () => !getPanelForMod(typeof(OsuModHidden)).Active.Value);
|
||||
|
||||
AddStep("clear search", () => modSelectOverlay.SearchTerm = string.Empty);
|
||||
AddStep("press enter", () => InputManager.Key(Key.Enter));
|
||||
AddAssert("mod select hidden", () => modSelectOverlay.State.Value == Visibility.Hidden);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSearchFocusChangeViaClick()
|
||||
{
|
||||
createScreen();
|
||||
|
||||
AddStep("click on search", navigateAndClick<ShearedSearchTextBox>);
|
||||
AddAssert("focused", () => modSelectOverlay.SearchTextBox.HasFocus);
|
||||
|
||||
AddStep("click on mod column", navigateAndClick<ModColumn>);
|
||||
AddAssert("lost focus", () => !modSelectOverlay.SearchTextBox.HasFocus);
|
||||
|
||||
void navigateAndClick<T>() where T : Drawable
|
||||
{
|
||||
InputManager.MoveMouseTo(modSelectOverlay.ChildrenOfType<T>().FirstOrDefault());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSearchFocusChangeViaKey()
|
||||
{
|
||||
createScreen();
|
||||
|
||||
const Key focus_switch_key = Key.Tab;
|
||||
|
||||
AddStep("press tab", () => InputManager.Key(focus_switch_key));
|
||||
AddAssert("focused", () => modSelectOverlay.SearchTextBox.HasFocus);
|
||||
|
||||
AddStep("press tab", () => InputManager.Key(focus_switch_key));
|
||||
AddAssert("lost focus", () => !modSelectOverlay.SearchTextBox.HasFocus);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -533,6 +583,8 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
createScreen();
|
||||
changeRuleset(0);
|
||||
|
||||
AddStep("kill search bar focus", () => modSelectOverlay.SearchTextBox.KillFocus());
|
||||
|
||||
AddStep("select DT + HD", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden() });
|
||||
AddAssert("DT + HD selected", () => modSelectOverlay.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value) == 2);
|
||||
|
||||
@@ -540,6 +592,26 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddUntilStep("all mods deselected", () => !SelectedMods.Value.Any());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDeselectAllViaKey_WithSearchApplied()
|
||||
{
|
||||
createScreen();
|
||||
changeRuleset(0);
|
||||
|
||||
AddStep("select DT + HD", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden() });
|
||||
AddStep("focus on search", () => modSelectOverlay.SearchTextBox.TakeFocus());
|
||||
AddStep("apply search", () => modSelectOverlay.SearchTerm = "Easy");
|
||||
AddAssert("DT + HD selected and hidden", () => modSelectOverlay.ChildrenOfType<ModPanel>().Count(panel => !panel.Visible && panel.Active.Value) == 2);
|
||||
|
||||
AddStep("press backspace", () => InputManager.Key(Key.BackSpace));
|
||||
AddAssert("DT + HD still selected", () => modSelectOverlay.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value) == 2);
|
||||
AddAssert("search term changed", () => modSelectOverlay.SearchTerm == "Eas");
|
||||
|
||||
AddStep("kill focus", () => modSelectOverlay.SearchTextBox.KillFocus());
|
||||
AddStep("press backspace", () => InputManager.Key(Key.BackSpace));
|
||||
AddUntilStep("all mods deselected", () => !SelectedMods.Value.Any());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDeselectAllViaButton()
|
||||
{
|
||||
@@ -561,6 +633,31 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddAssert("deselect all button disabled", () => !this.ChildrenOfType<DeselectAllModsButton>().Single().Enabled.Value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDeselectAllViaButton_WithSearchApplied()
|
||||
{
|
||||
createScreen();
|
||||
changeRuleset(0);
|
||||
|
||||
AddAssert("deselect all button disabled", () => !this.ChildrenOfType<DeselectAllModsButton>().Single().Enabled.Value);
|
||||
|
||||
AddStep("select DT + HD + RD", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden(), new OsuModRandom() });
|
||||
AddAssert("DT + HD + RD selected", () => modSelectOverlay.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value) == 3);
|
||||
AddAssert("deselect all button enabled", () => this.ChildrenOfType<DeselectAllModsButton>().Single().Enabled.Value);
|
||||
|
||||
AddStep("apply search", () => modSelectOverlay.SearchTerm = "Easy");
|
||||
AddAssert("DT + HD + RD are hidden and selected", () => modSelectOverlay.ChildrenOfType<ModPanel>().Count(panel => !panel.Visible && panel.Active.Value) == 3);
|
||||
AddAssert("deselect all button enabled", () => this.ChildrenOfType<DeselectAllModsButton>().Single().Enabled.Value);
|
||||
|
||||
AddStep("click deselect all button", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<DeselectAllModsButton>().Single());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddUntilStep("all mods deselected", () => !SelectedMods.Value.Any());
|
||||
AddAssert("deselect all button disabled", () => !this.ChildrenOfType<DeselectAllModsButton>().Single().Enabled.Value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCloseViaBackButton()
|
||||
{
|
||||
@@ -580,8 +677,11 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddAssert("mod select hidden", () => modSelectOverlay.State.Value == Visibility.Hidden);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Covers columns hiding/unhiding on changes of <see cref="ModSelectOverlay.IsValidMod"/>.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestColumnHiding()
|
||||
public void TestColumnHidingOnIsValidChange()
|
||||
{
|
||||
AddStep("create screen", () => Child = modSelectOverlay = new TestModSelectOverlay
|
||||
{
|
||||
@@ -610,6 +710,56 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddUntilStep("3 columns visible", () => this.ChildrenOfType<ModColumn>().Count(col => col.IsPresent) == 3);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Covers columns hiding/unhiding on changes of <see cref="ModSelectOverlay.SearchTerm"/>.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestColumnHidingOnTextFilterChange()
|
||||
{
|
||||
AddStep("create screen", () => Child = modSelectOverlay = new TestModSelectOverlay
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
State = { Value = Visibility.Visible },
|
||||
SelectedMods = { BindTarget = SelectedMods }
|
||||
});
|
||||
waitForColumnLoad();
|
||||
changeRuleset(0);
|
||||
|
||||
AddAssert("all columns visible", () => this.ChildrenOfType<ModColumn>().All(col => col.IsPresent));
|
||||
|
||||
AddStep("set search", () => modSelectOverlay.SearchTerm = "HD");
|
||||
AddAssert("one column visible", () => this.ChildrenOfType<ModColumn>().Count(col => col.IsPresent) == 1);
|
||||
|
||||
AddStep("filter out everything", () => modSelectOverlay.SearchTerm = "Some long search term with no matches");
|
||||
AddAssert("no columns visible", () => this.ChildrenOfType<ModColumn>().All(col => !col.IsPresent));
|
||||
|
||||
AddStep("clear search bar", () => modSelectOverlay.SearchTerm = "");
|
||||
AddAssert("all columns visible", () => this.ChildrenOfType<ModColumn>().All(col => col.IsPresent));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHidingOverlayClearsTextSearch()
|
||||
{
|
||||
AddStep("create screen", () => Child = modSelectOverlay = new TestModSelectOverlay
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
State = { Value = Visibility.Visible },
|
||||
SelectedMods = { BindTarget = SelectedMods }
|
||||
});
|
||||
waitForColumnLoad();
|
||||
changeRuleset(0);
|
||||
|
||||
AddAssert("all columns visible", () => this.ChildrenOfType<ModColumn>().All(col => col.IsPresent));
|
||||
|
||||
AddStep("set search", () => modSelectOverlay.SearchTerm = "fail");
|
||||
AddAssert("one column visible", () => this.ChildrenOfType<ModColumn>().Count(col => col.IsPresent) == 2);
|
||||
|
||||
AddStep("hide", () => modSelectOverlay.Hide());
|
||||
AddStep("show", () => modSelectOverlay.Show());
|
||||
|
||||
AddAssert("all columns visible", () => this.ChildrenOfType<ModColumn>().All(col => col.IsPresent));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestColumnHidingOnRulesetChange()
|
||||
{
|
||||
@@ -688,12 +838,10 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
public override string ShortName => "unimplemented";
|
||||
|
||||
public override IEnumerable<Mod> GetModsFor(ModType type)
|
||||
{
|
||||
if (type == ModType.Conversion) return base.GetModsFor(type).Concat(new[] { new TestUnimplementedMod() });
|
||||
|
||||
return base.GetModsFor(type);
|
||||
}
|
||||
public override IEnumerable<Mod> GetModsFor(ModType type) =>
|
||||
type == ModType.Conversion
|
||||
? base.GetModsFor(type).Concat(new[] { new TestUnimplementedMod() })
|
||||
: base.GetModsFor(type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,6 +101,10 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +80,24 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCorrectScrollToWhenContentLoads()
|
||||
{
|
||||
AddRepeatStep("add many sections", () => append(1f), 3);
|
||||
|
||||
AddStep("add section with delayed load content", () =>
|
||||
{
|
||||
container.Add(new TestDelayedLoadSection("delayed"));
|
||||
});
|
||||
|
||||
AddStep("add final section", () => append(0.5f));
|
||||
|
||||
AddStep("scroll to final section", () => container.ScrollTo(container.Children.Last()));
|
||||
|
||||
AddUntilStep("correct section selected", () => container.SelectedSection.Value == container.Children.Last());
|
||||
AddUntilStep("wait for scroll to section", () => container.ScreenSpaceDrawQuad.AABBFloat.Contains(container.Children.Last().ScreenSpaceDrawQuad.AABBFloat));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSelection()
|
||||
{
|
||||
@@ -196,6 +214,33 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
InputManager.ScrollVerticalBy(direction);
|
||||
}
|
||||
|
||||
private partial class TestDelayedLoadSection : TestSection
|
||||
{
|
||||
public TestDelayedLoadSection(string label)
|
||||
: base(label)
|
||||
{
|
||||
BackgroundColour = default_colour;
|
||||
Width = 300;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Box box;
|
||||
|
||||
Add(box = new Box
|
||||
{
|
||||
Alpha = 0.01f,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
});
|
||||
|
||||
// Emulate an operation that will be inhibited by IsMaskedAway.
|
||||
box.ResizeHeightTo(2000, 50);
|
||||
}
|
||||
}
|
||||
|
||||
private partial class TestSection : TestBox
|
||||
{
|
||||
public bool Selected
|
||||
|
||||
@@ -51,7 +51,7 @@ namespace osu.Game.Tests
|
||||
|
||||
protected override IBeatmap GetBeatmap() => beatmap;
|
||||
|
||||
protected override Texture GetBackground() => null;
|
||||
public override Texture GetBackground() => null;
|
||||
|
||||
protected override Waveform GetWaveform() => new Waveform(trackStore.GetStream(firstAudioFile));
|
||||
|
||||
|
||||
@@ -24,6 +24,9 @@ namespace osu.Game.Tournament.Tests.Screens
|
||||
Add(screen = new MapPoolScreen { Width = 0.7f });
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() => Ladder.SplitMapPoolByMods.Value = true);
|
||||
|
||||
[Test]
|
||||
public void TestFewMaps()
|
||||
{
|
||||
@@ -92,7 +95,7 @@ namespace osu.Game.Tournament.Tests.Screens
|
||||
Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Clear();
|
||||
|
||||
for (int i = 0; i < 11; i++)
|
||||
addBeatmap(i > 4 ? $"M{i}" : "NM");
|
||||
addBeatmap(i > 4 ? Ruleset.Value.CreateInstance().AllMods.ElementAt(i).Acronym : "NM");
|
||||
});
|
||||
|
||||
AddStep("reset match", () =>
|
||||
@@ -118,7 +121,7 @@ namespace osu.Game.Tournament.Tests.Screens
|
||||
Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Clear();
|
||||
|
||||
for (int i = 0; i < 12; i++)
|
||||
addBeatmap(i > 4 ? $"M{i}" : "NM");
|
||||
addBeatmap(i > 4 ? Ruleset.Value.CreateInstance().AllMods.ElementAt(i).Acronym : "NM");
|
||||
});
|
||||
|
||||
AddStep("reset match", () =>
|
||||
@@ -130,7 +133,27 @@ namespace osu.Game.Tournament.Tests.Screens
|
||||
assertThreeWide();
|
||||
}
|
||||
|
||||
private void addBeatmap(string mods = "nm")
|
||||
[Test]
|
||||
public void TestSplitMapPoolByMods()
|
||||
{
|
||||
AddStep("load many maps", () =>
|
||||
{
|
||||
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");
|
||||
});
|
||||
|
||||
AddStep("disable splitting map pool by mods", () => Ladder.SplitMapPoolByMods.Value = false);
|
||||
|
||||
AddStep("reset match", () =>
|
||||
{
|
||||
Ladder.CurrentMatch.Value = new TournamentMatch();
|
||||
Ladder.CurrentMatch.Value = Ladder.Matches.First();
|
||||
});
|
||||
}
|
||||
|
||||
private void addBeatmap(string mods = "NM")
|
||||
{
|
||||
Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Add(new RoundBeatmap
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
namespace osu.Game.Tournament.IPC
|
||||
{
|
||||
public enum TourneyState
|
||||
|
||||
@@ -42,5 +42,7 @@ namespace osu.Game.Tournament.Models
|
||||
};
|
||||
|
||||
public Bindable<bool> AutoProgressScreens = new BindableBool(true);
|
||||
|
||||
public Bindable<bool> SplitMapPoolByMods = new BindableBool(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 osu.Game.Tournament.Models;
|
||||
|
||||
namespace osu.Game.Tournament.Screens.Ladder.Components
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace osu.Game.Tournament.Screens.MapPool
|
||||
{
|
||||
public partial class MapPoolScreen : TournamentMatchScreen
|
||||
{
|
||||
private readonly FillFlowContainer<FillFlowContainer<TournamentBeatmapPanel>> mapFlows;
|
||||
private FillFlowContainer<FillFlowContainer<TournamentBeatmapPanel>> mapFlows;
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private TournamentSceneManager sceneManager { get; set; }
|
||||
@@ -32,12 +32,13 @@ namespace osu.Game.Tournament.Screens.MapPool
|
||||
private TeamColour pickColour;
|
||||
private ChoiceType pickType;
|
||||
|
||||
private readonly OsuButton buttonRedBan;
|
||||
private readonly OsuButton buttonBlueBan;
|
||||
private readonly OsuButton buttonRedPick;
|
||||
private readonly OsuButton buttonBluePick;
|
||||
private OsuButton buttonRedBan;
|
||||
private OsuButton buttonBlueBan;
|
||||
private OsuButton buttonRedPick;
|
||||
private OsuButton buttonBluePick;
|
||||
|
||||
public MapPoolScreen()
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(MatchIPCInfo ipc)
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
@@ -98,15 +99,26 @@ namespace osu.Game.Tournament.Screens.MapPool
|
||||
Action = reset
|
||||
},
|
||||
new ControlPanel.Spacer(),
|
||||
new OsuCheckbox
|
||||
{
|
||||
LabelText = "Split display by mods",
|
||||
Current = LadderInfo.SplitMapPoolByMods,
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
ipc.Beatmap.BindValueChanged(beatmapChanged);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(MatchIPCInfo ipc)
|
||||
private Bindable<bool> splitMapPoolByMods;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
ipc.Beatmap.BindValueChanged(beatmapChanged);
|
||||
base.LoadComplete();
|
||||
|
||||
splitMapPoolByMods = LadderInfo.SplitMapPoolByMods.GetBoundCopy();
|
||||
splitMapPoolByMods.BindValueChanged(_ => updateDisplay());
|
||||
}
|
||||
|
||||
private void beatmapChanged(ValueChangedEvent<TournamentBeatmap> beatmap)
|
||||
@@ -213,24 +225,27 @@ namespace osu.Game.Tournament.Screens.MapPool
|
||||
protected override void CurrentMatchChanged(ValueChangedEvent<TournamentMatch> match)
|
||||
{
|
||||
base.CurrentMatchChanged(match);
|
||||
updateDisplay();
|
||||
}
|
||||
|
||||
private void updateDisplay()
|
||||
{
|
||||
mapFlows.Clear();
|
||||
|
||||
if (match.NewValue == null)
|
||||
if (CurrentMatch.Value == null)
|
||||
return;
|
||||
|
||||
int totalRows = 0;
|
||||
|
||||
if (match.NewValue.Round.Value != null)
|
||||
if (CurrentMatch.Value.Round.Value != null)
|
||||
{
|
||||
FillFlowContainer<TournamentBeatmapPanel> currentFlow = null;
|
||||
string currentMod = null;
|
||||
|
||||
string currentMods = null;
|
||||
int flowCount = 0;
|
||||
|
||||
foreach (var b in match.NewValue.Round.Value.Beatmaps)
|
||||
foreach (var b in CurrentMatch.Value.Round.Value.Beatmaps)
|
||||
{
|
||||
if (currentFlow == null || currentMod != b.Mods)
|
||||
if (currentFlow == null || (LadderInfo.SplitMapPoolByMods.Value && currentMods != b.Mods))
|
||||
{
|
||||
mapFlows.Add(currentFlow = new FillFlowContainer<TournamentBeatmapPanel>
|
||||
{
|
||||
@@ -240,7 +255,7 @@ namespace osu.Game.Tournament.Screens.MapPool
|
||||
AutoSizeAxes = Axes.Y
|
||||
});
|
||||
|
||||
currentMod = b.Mods;
|
||||
currentMods = b.Mods;
|
||||
|
||||
totalRows++;
|
||||
flowCount = 0;
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
// 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.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.IO.Stores;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
|
||||
namespace osu.Game.Beatmaps
|
||||
{
|
||||
// Implementation of this class is based off of `MaxDimensionLimitedTextureLoaderStore`.
|
||||
// If issues are found it's worth checking to make sure similar issues exist there.
|
||||
public class BeatmapPanelBackgroundTextureLoaderStore : IResourceStore<TextureUpload>
|
||||
{
|
||||
// The aspect ratio of SetPanelBackground at its maximum size (very tall window).
|
||||
private const float minimum_display_ratio = 512 / 80f;
|
||||
|
||||
private readonly IResourceStore<TextureUpload>? textureStore;
|
||||
|
||||
public BeatmapPanelBackgroundTextureLoaderStore(IResourceStore<TextureUpload>? textureStore)
|
||||
{
|
||||
this.textureStore = textureStore;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
textureStore?.Dispose();
|
||||
}
|
||||
|
||||
public TextureUpload Get(string name)
|
||||
{
|
||||
var textureUpload = textureStore?.Get(name);
|
||||
|
||||
// NRT not enabled on framework side classes (IResourceStore / TextureLoaderStore), welp.
|
||||
if (textureUpload == null)
|
||||
return null!;
|
||||
|
||||
return limitTextureUploadSize(textureUpload);
|
||||
}
|
||||
|
||||
public async Task<TextureUpload> GetAsync(string name, CancellationToken cancellationToken = new CancellationToken())
|
||||
{
|
||||
// NRT not enabled on framework side classes (IResourceStore / TextureLoaderStore), welp.
|
||||
if (textureStore == null)
|
||||
return null!;
|
||||
|
||||
var textureUpload = await textureStore.GetAsync(name, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (textureUpload == null)
|
||||
return null!;
|
||||
|
||||
return await Task.Run(() => limitTextureUploadSize(textureUpload), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private TextureUpload limitTextureUploadSize(TextureUpload textureUpload)
|
||||
{
|
||||
var image = Image.LoadPixelData(textureUpload.Data.ToArray(), textureUpload.Width, textureUpload.Height);
|
||||
|
||||
// The original texture upload will no longer be returned or used.
|
||||
textureUpload.Dispose();
|
||||
|
||||
Size size = image.Size();
|
||||
|
||||
// Assume that panel backgrounds are always displayed using `FillMode.Fill`.
|
||||
// Also assume that all backgrounds are wider than they are tall, so the
|
||||
// fill is always going to be based on width.
|
||||
//
|
||||
// We need to include enough height to make this work for all ratio panels are displayed at.
|
||||
int usableHeight = (int)Math.Ceiling(size.Width * 1 / minimum_display_ratio);
|
||||
|
||||
usableHeight = Math.Min(size.Height, usableHeight);
|
||||
|
||||
// Crop the centre region of the background for now.
|
||||
Rectangle cropRectangle = new Rectangle(
|
||||
0,
|
||||
(size.Height - usableHeight) / 2,
|
||||
size.Width,
|
||||
usableHeight
|
||||
);
|
||||
|
||||
image.Mutate(i => i.Crop(cropRectangle));
|
||||
|
||||
return new TextureUpload(image);
|
||||
}
|
||||
|
||||
public Stream? GetStream(string name) => textureStore?.GetStream(name);
|
||||
|
||||
public IEnumerable<string> GetAvailableResources() => textureStore?.GetAvailableResources() ?? Array.Empty<string>();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
namespace osu.Game.Beatmaps
|
||||
{
|
||||
public enum DifficultyRating
|
||||
|
||||
@@ -23,8 +23,9 @@ namespace osu.Game.Beatmaps.Drawables
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
if (working.Background != null)
|
||||
Texture = working.Background;
|
||||
var background = working.GetBackground();
|
||||
if (background != null)
|
||||
Texture = background;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,12 +106,11 @@ namespace osu.Game.Beatmaps.Drawables.Cards
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
new TruncatingSpriteText
|
||||
{
|
||||
Text = new RomanisableString(BeatmapSet.TitleUnicode, BeatmapSet.Title),
|
||||
Font = OsuFont.Default.With(size: 22.5f, weight: FontWeight.SemiBold),
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Truncate = true
|
||||
},
|
||||
titleBadgeArea = new FillFlowContainer
|
||||
{
|
||||
@@ -140,21 +139,19 @@ namespace osu.Game.Beatmaps.Drawables.Cards
|
||||
{
|
||||
new[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
new TruncatingSpriteText
|
||||
{
|
||||
Text = createArtistText(),
|
||||
Font = OsuFont.Default.With(size: 17.5f, weight: FontWeight.SemiBold),
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Truncate = true
|
||||
},
|
||||
Empty()
|
||||
},
|
||||
}
|
||||
},
|
||||
new OsuSpriteText
|
||||
new TruncatingSpriteText
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Truncate = true,
|
||||
Text = BeatmapSet.Source,
|
||||
Shadow = false,
|
||||
Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold),
|
||||
|
||||
@@ -107,12 +107,11 @@ namespace osu.Game.Beatmaps.Drawables.Cards
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
new TruncatingSpriteText
|
||||
{
|
||||
Text = new RomanisableString(BeatmapSet.TitleUnicode, BeatmapSet.Title),
|
||||
Font = OsuFont.Default.With(size: 22.5f, weight: FontWeight.SemiBold),
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Truncate = true
|
||||
},
|
||||
titleBadgeArea = new FillFlowContainer
|
||||
{
|
||||
@@ -141,12 +140,11 @@ namespace osu.Game.Beatmaps.Drawables.Cards
|
||||
{
|
||||
new[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
new TruncatingSpriteText
|
||||
{
|
||||
Text = createArtistText(),
|
||||
Font = OsuFont.Default.With(size: 17.5f, weight: FontWeight.SemiBold),
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Truncate = true
|
||||
},
|
||||
Empty()
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
namespace osu.Game.Beatmaps.Drawables.Cards
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -52,7 +52,7 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
protected override IBeatmap GetBeatmap() => new Beatmap();
|
||||
|
||||
protected override Texture GetBackground() => textures?.Get(@"Backgrounds/bg4");
|
||||
public override Texture GetBackground() => textures?.Get(@"Backgrounds/bg4");
|
||||
|
||||
protected override Track GetBeatmapTrack() => GetVirtualTrack();
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ namespace osu.Game.Beatmaps
|
||||
}
|
||||
|
||||
protected override IBeatmap GetBeatmap() => beatmap;
|
||||
protected override Texture GetBackground() => throw new NotImplementedException();
|
||||
public override Texture GetBackground() => throw new NotImplementedException();
|
||||
protected override Track GetBeatmapTrack() => throw new NotImplementedException();
|
||||
protected internal override ISkin GetSkin() => throw new NotImplementedException();
|
||||
public override Stream GetStream(string storagePath) => throw new NotImplementedException();
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace osu.Game.Beatmaps
|
||||
IBeatmapSetInfo? BeatmapSet { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The playable length in milliseconds of this beatmap.
|
||||
/// The total length in milliseconds of this beatmap.
|
||||
/// </summary>
|
||||
double Length { get; }
|
||||
|
||||
|
||||
@@ -59,5 +59,10 @@ namespace osu.Game.Beatmaps
|
||||
int PassCount { get; }
|
||||
|
||||
APIFailTimes? FailTimes { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The playable length in milliseconds of this beatmap.
|
||||
/// </summary>
|
||||
double HitLength { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,13 +9,18 @@ using osu.Game.IO;
|
||||
|
||||
namespace osu.Game.Beatmaps
|
||||
{
|
||||
public interface IBeatmapResourceProvider : IStorageResourceProvider
|
||||
internal interface IBeatmapResourceProvider : IStorageResourceProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieve a global large texture store, used for loading beatmap backgrounds.
|
||||
/// </summary>
|
||||
TextureStore LargeTextureStore { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve a global large texture store, used specifically for retrieving cropped beatmap panel backgrounds.
|
||||
/// </summary>
|
||||
TextureStore BeatmapPanelTextureStore { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Access a global track store for retrieving beatmap tracks from.
|
||||
/// </summary>
|
||||
|
||||
@@ -32,12 +32,12 @@ namespace osu.Game.Beatmaps
|
||||
/// <summary>
|
||||
/// Whether the Beatmap has finished loading.
|
||||
///</summary>
|
||||
public bool BeatmapLoaded { get; }
|
||||
bool BeatmapLoaded { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the Track has finished loading.
|
||||
///</summary>
|
||||
public bool TrackLoaded { get; }
|
||||
bool TrackLoaded { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the <see cref="IBeatmap"/> which this <see cref="IWorkingBeatmap"/> represents.
|
||||
@@ -47,7 +47,12 @@ namespace osu.Game.Beatmaps
|
||||
/// <summary>
|
||||
/// Retrieves the background for this <see cref="IWorkingBeatmap"/>.
|
||||
/// </summary>
|
||||
Texture Background { get; }
|
||||
Texture GetBackground();
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a cropped background for this <see cref="IWorkingBeatmap"/> used for display on panels.
|
||||
/// </summary>
|
||||
Texture GetPanelBackground();
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the <see cref="Waveform"/> for the <see cref="Track"/> of this <see cref="IWorkingBeatmap"/>.
|
||||
@@ -124,12 +129,12 @@ namespace osu.Game.Beatmaps
|
||||
/// <summary>
|
||||
/// Beings loading the contents of this <see cref="IWorkingBeatmap"/> asynchronously.
|
||||
/// </summary>
|
||||
public void BeginAsyncLoad();
|
||||
void BeginAsyncLoad();
|
||||
|
||||
/// <summary>
|
||||
/// Cancels the asynchronous loading of the contents of this <see cref="IWorkingBeatmap"/>.
|
||||
/// </summary>
|
||||
public void CancelAsyncLoad();
|
||||
void CancelAsyncLoad();
|
||||
|
||||
/// <summary>
|
||||
/// Reads the correct track restart point from beatmap metadata and sets looping to enabled.
|
||||
|
||||
@@ -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
|
||||
|
||||
namespace osu.Game.Beatmaps.Legacy
|
||||
{
|
||||
internal enum LegacyOrigins
|
||||
|
||||
@@ -34,8 +34,6 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
public Storyboard Storyboard => storyboard.Value;
|
||||
|
||||
public Texture Background => GetBackground(); // Texture uses ref counting, so we want to return a new instance every usage.
|
||||
|
||||
public ISkin Skin => skin.Value;
|
||||
|
||||
private AudioManager audioManager { get; }
|
||||
@@ -67,7 +65,8 @@ namespace osu.Game.Beatmaps
|
||||
protected virtual Storyboard GetStoryboard() => new Storyboard { BeatmapInfo = BeatmapInfo };
|
||||
|
||||
protected abstract IBeatmap GetBeatmap();
|
||||
protected abstract Texture GetBackground();
|
||||
public abstract Texture GetBackground();
|
||||
public virtual Texture GetPanelBackground() => GetBackground();
|
||||
protected abstract Track GetBeatmapTrack();
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -42,6 +42,7 @@ namespace osu.Game.Beatmaps
|
||||
private readonly AudioManager audioManager;
|
||||
private readonly IResourceStore<byte[]> resources;
|
||||
private readonly LargeTextureStore largeTextureStore;
|
||||
private readonly LargeTextureStore beatmapPanelTextureStore;
|
||||
private readonly ITrackStore trackStore;
|
||||
private readonly IResourceStore<byte[]> files;
|
||||
|
||||
@@ -58,6 +59,7 @@ namespace osu.Game.Beatmaps
|
||||
this.host = host;
|
||||
this.files = files;
|
||||
largeTextureStore = new LargeTextureStore(host?.Renderer ?? new DummyRenderer(), host?.CreateTextureLoaderStore(files));
|
||||
beatmapPanelTextureStore = new LargeTextureStore(host?.Renderer ?? new DummyRenderer(), new BeatmapPanelBackgroundTextureLoaderStore(host?.CreateTextureLoaderStore(files)));
|
||||
this.trackStore = trackStore;
|
||||
}
|
||||
|
||||
@@ -110,6 +112,7 @@ namespace osu.Game.Beatmaps
|
||||
#region IResourceStorageProvider
|
||||
|
||||
TextureStore IBeatmapResourceProvider.LargeTextureStore => largeTextureStore;
|
||||
TextureStore IBeatmapResourceProvider.BeatmapPanelTextureStore => beatmapPanelTextureStore;
|
||||
ITrackStore IBeatmapResourceProvider.Tracks => trackStore;
|
||||
IRenderer IStorageResourceProvider.Renderer => host?.Renderer ?? new DummyRenderer();
|
||||
AudioManager IStorageResourceProvider.AudioManager => audioManager;
|
||||
@@ -160,7 +163,11 @@ namespace osu.Game.Beatmaps
|
||||
}
|
||||
}
|
||||
|
||||
protected override Texture GetBackground()
|
||||
public override Texture GetPanelBackground() => getBackgroundFromStore(resources.BeatmapPanelTextureStore);
|
||||
|
||||
public override Texture GetBackground() => getBackgroundFromStore(resources.LargeTextureStore);
|
||||
|
||||
private Texture getBackgroundFromStore(TextureStore store)
|
||||
{
|
||||
if (string.IsNullOrEmpty(Metadata?.BackgroundFile))
|
||||
return null;
|
||||
@@ -168,7 +175,7 @@ namespace osu.Game.Beatmaps
|
||||
try
|
||||
{
|
||||
string fileStorePath = BeatmapSetInfo.GetPathForFile(Metadata.BackgroundFile);
|
||||
var texture = resources.LargeTextureStore.Get(fileStorePath);
|
||||
var texture = store.Get(fileStorePath);
|
||||
|
||||
if (texture == null)
|
||||
{
|
||||
@@ -257,7 +264,7 @@ namespace osu.Game.Beatmaps
|
||||
if (beatmapFileStream == null)
|
||||
{
|
||||
Logger.Log($"Beatmap failed to load (file {BeatmapInfo.Path} not found on disk at expected location {fileStorePath})", level: LogLevel.Error);
|
||||
return null;
|
||||
return new Storyboard();
|
||||
}
|
||||
|
||||
using (var reader = new LineBufferedReader(beatmapFileStream))
|
||||
|
||||
@@ -114,8 +114,6 @@ namespace osu.Game.Collections
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
base.PopIn();
|
||||
|
||||
lowPassFilter.CutoffTo(300, 100, Easing.OutCubic);
|
||||
this.FadeIn(enter_duration, Easing.OutQuint);
|
||||
this.ScaleTo(0.9f).Then().ScaleTo(1f, enter_duration, Easing.OutQuint);
|
||||
|
||||
@@ -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
|
||||
|
||||
namespace osu.Game.Configuration
|
||||
{
|
||||
public enum IntroSequence
|
||||
|
||||
@@ -178,6 +178,7 @@ namespace osu.Game.Configuration
|
||||
SetDefault(OsuSetting.EditorWaveformOpacity, 0.25f, 0f, 1f, 0.25f);
|
||||
SetDefault(OsuSetting.EditorShowHitMarkers, true);
|
||||
SetDefault(OsuSetting.EditorAutoSeekOnPlacement, true);
|
||||
SetDefault(OsuSetting.EditorLimitedDistanceSnap, false);
|
||||
|
||||
SetDefault(OsuSetting.LastProcessedMetadataId, -1);
|
||||
|
||||
@@ -383,5 +384,6 @@ namespace osu.Game.Configuration
|
||||
SafeAreaConsiderations,
|
||||
ComboColourNormalisationAmount,
|
||||
ProfileCoverExpanded,
|
||||
EditorLimitedDistanceSnap
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
namespace osu.Game.Configuration
|
||||
{
|
||||
public enum ReleaseStream
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Mods;
|
||||
|
||||
namespace osu.Game.Configuration
|
||||
{
|
||||
@@ -21,6 +22,7 @@ namespace osu.Game.Configuration
|
||||
SetDefault(Static.LowBatteryNotificationShownOnce, false);
|
||||
SetDefault(Static.FeaturedArtistDisclaimerShownOnce, false);
|
||||
SetDefault(Static.LastHoverSoundPlaybackTime, (double?)null);
|
||||
SetDefault(Static.LastModSelectPanelSamplePlaybackTime, (double?)null);
|
||||
SetDefault<APISeasonalBackgrounds>(Static.SeasonalBackgrounds, null);
|
||||
}
|
||||
|
||||
@@ -56,5 +58,11 @@ namespace osu.Game.Configuration
|
||||
/// Used to debounce hover sounds game-wide to avoid volume saturation, especially in scrolling views with many UI controls like <see cref="SettingsOverlay"/>.
|
||||
/// </summary>
|
||||
LastHoverSoundPlaybackTime,
|
||||
|
||||
/// <summary>
|
||||
/// The last playback time in milliseconds of an on/off sample (from <see cref="ModSelectPanel"/>).
|
||||
/// Used to debounce <see cref="ModSelectPanel"/> on/off sounds game-wide to avoid volume saturation, especially in activating mod presets with many mods.
|
||||
/// </summary>
|
||||
LastModSelectPanelSamplePlaybackTime
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
namespace osu.Game.Configuration
|
||||
{
|
||||
public enum ToolbarClockDisplayMode
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
|
||||
@@ -21,8 +18,7 @@ namespace osu.Game.Database
|
||||
/// <param name="beatmapId">The beatmap to lookup.</param>
|
||||
/// <param name="token">An optional cancellation token.</param>
|
||||
/// <returns>The populated beatmap, or null if the beatmap does not exist or the request could not be satisfied.</returns>
|
||||
[ItemCanBeNull]
|
||||
public Task<APIBeatmap> GetBeatmapAsync(int beatmapId, CancellationToken token = default) => LookupAsync(beatmapId, token);
|
||||
public Task<APIBeatmap?> GetBeatmapAsync(int beatmapId, CancellationToken token = default) => LookupAsync(beatmapId, token);
|
||||
|
||||
/// <summary>
|
||||
/// Perform an API lookup on the specified beatmaps, populating a <see cref="APIBeatmap"/> model.
|
||||
@@ -30,10 +26,10 @@ namespace osu.Game.Database
|
||||
/// <param name="beatmapIds">The beatmaps to lookup.</param>
|
||||
/// <param name="token">An optional cancellation token.</param>
|
||||
/// <returns>The populated beatmaps. May include null results for failed retrievals.</returns>
|
||||
public Task<APIBeatmap[]> GetBeatmapsAsync(int[] beatmapIds, CancellationToken token = default) => LookupAsync(beatmapIds, token);
|
||||
public Task<APIBeatmap?[]> GetBeatmapsAsync(int[] beatmapIds, CancellationToken token = default) => LookupAsync(beatmapIds, token);
|
||||
|
||||
protected override GetBeatmapsRequest CreateRequest(IEnumerable<int> ids) => new GetBeatmapsRequest(ids.ToArray());
|
||||
|
||||
protected override IEnumerable<APIBeatmap> RetrieveResults(GetBeatmapsRequest request) => request.Response?.Beatmaps;
|
||||
protected override IEnumerable<APIBeatmap>? RetrieveResults(GetBeatmapsRequest request) => request.Response?.Beatmaps;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,13 +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.Concurrent;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Extensions.TypeExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Statistics;
|
||||
@@ -19,8 +17,9 @@ namespace osu.Game.Database
|
||||
/// Currently not persisted between game sessions.
|
||||
/// </summary>
|
||||
public abstract partial class MemoryCachingComponent<TLookup, TValue> : Component
|
||||
where TLookup : notnull
|
||||
{
|
||||
private readonly ConcurrentDictionary<TLookup, TValue> cache = new ConcurrentDictionary<TLookup, TValue>();
|
||||
private readonly ConcurrentDictionary<TLookup, TValue?> cache = new ConcurrentDictionary<TLookup, TValue?>();
|
||||
|
||||
private readonly GlobalStatistic<MemoryCachingStatistics> statistics;
|
||||
|
||||
@@ -37,12 +36,12 @@ namespace osu.Game.Database
|
||||
/// </summary>
|
||||
/// <param name="lookup">The lookup to retrieve.</param>
|
||||
/// <param name="token">An optional <see cref="CancellationToken"/> to cancel the operation.</param>
|
||||
protected async Task<TValue> GetAsync([NotNull] TLookup lookup, CancellationToken token = default)
|
||||
protected async Task<TValue?> GetAsync(TLookup lookup, CancellationToken token = default)
|
||||
{
|
||||
if (CheckExists(lookup, out TValue performance))
|
||||
if (CheckExists(lookup, out TValue? existing))
|
||||
{
|
||||
statistics.Value.HitCount++;
|
||||
return performance;
|
||||
return existing;
|
||||
}
|
||||
|
||||
var computed = await ComputeValueAsync(lookup, token).ConfigureAwait(false);
|
||||
@@ -73,7 +72,7 @@ namespace osu.Game.Database
|
||||
statistics.Value.Usage = cache.Count;
|
||||
}
|
||||
|
||||
protected bool CheckExists([NotNull] TLookup lookup, out TValue value) =>
|
||||
protected bool CheckExists(TLookup lookup, [MaybeNullWhen(false)] out TValue value) =>
|
||||
cache.TryGetValue(lookup, out value);
|
||||
|
||||
/// <summary>
|
||||
@@ -82,7 +81,7 @@ namespace osu.Game.Database
|
||||
/// <param name="lookup">The lookup to retrieve.</param>
|
||||
/// <param name="token">An optional <see cref="CancellationToken"/> to cancel the operation.</param>
|
||||
/// <returns>The computed value.</returns>
|
||||
protected abstract Task<TValue> ComputeValueAsync(TLookup lookup, CancellationToken token = default);
|
||||
protected abstract Task<TValue?> ComputeValueAsync(TLookup lookup, CancellationToken token = default);
|
||||
|
||||
private class MemoryCachingStatistics
|
||||
{
|
||||
|
||||
@@ -1,14 +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.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Game.Online.API;
|
||||
@@ -21,7 +18,7 @@ namespace osu.Game.Database
|
||||
where TRequest : APIRequest
|
||||
{
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; }
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="APIRequest"/> to retrieve the values for a given collection of <typeparamref name="TLookup"/>s.
|
||||
@@ -32,8 +29,7 @@ namespace osu.Game.Database
|
||||
/// <summary>
|
||||
/// Retrieves a list of <typeparamref name="TValue"/>s from a successful <typeparamref name="TRequest"/> created by <see cref="CreateRequest"/>.
|
||||
/// </summary>
|
||||
[CanBeNull]
|
||||
protected abstract IEnumerable<TValue> RetrieveResults(TRequest request);
|
||||
protected abstract IEnumerable<TValue>? RetrieveResults(TRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// Perform a lookup using the specified <paramref name="id"/>, populating a <typeparamref name="TValue"/>.
|
||||
@@ -41,8 +37,7 @@ namespace osu.Game.Database
|
||||
/// <param name="id">The ID to lookup.</param>
|
||||
/// <param name="token">An optional cancellation token.</param>
|
||||
/// <returns>The populated <typeparamref name="TValue"/>, or null if the value does not exist or the request could not be satisfied.</returns>
|
||||
[ItemCanBeNull]
|
||||
protected Task<TValue> LookupAsync(TLookup id, CancellationToken token = default) => GetAsync(id, token);
|
||||
protected Task<TValue?> LookupAsync(TLookup id, CancellationToken token = default) => GetAsync(id, token);
|
||||
|
||||
/// <summary>
|
||||
/// Perform an API lookup on the specified <paramref name="ids"/>, populating a <typeparamref name="TValue"/>.
|
||||
@@ -50,9 +45,9 @@ namespace osu.Game.Database
|
||||
/// <param name="ids">The IDs to lookup.</param>
|
||||
/// <param name="token">An optional cancellation token.</param>
|
||||
/// <returns>The populated values. May include null results for failed retrievals.</returns>
|
||||
protected Task<TValue[]> LookupAsync(TLookup[] ids, CancellationToken token = default)
|
||||
protected Task<TValue?[]> LookupAsync(TLookup[] ids, CancellationToken token = default)
|
||||
{
|
||||
var lookupTasks = new List<Task<TValue>>();
|
||||
var lookupTasks = new List<Task<TValue?>>();
|
||||
|
||||
foreach (var id in ids)
|
||||
{
|
||||
@@ -69,18 +64,18 @@ namespace osu.Game.Database
|
||||
}
|
||||
|
||||
// cannot be sealed due to test usages (see TestUserLookupCache).
|
||||
protected override async Task<TValue> ComputeValueAsync(TLookup lookup, CancellationToken token = default)
|
||||
protected override async Task<TValue?> ComputeValueAsync(TLookup lookup, CancellationToken token = default)
|
||||
=> await queryValue(lookup).ConfigureAwait(false);
|
||||
|
||||
private readonly Queue<(TLookup id, TaskCompletionSource<TValue>)> pendingTasks = new Queue<(TLookup, TaskCompletionSource<TValue>)>();
|
||||
private Task pendingRequestTask;
|
||||
private readonly Queue<(TLookup id, TaskCompletionSource<TValue?>)> pendingTasks = new Queue<(TLookup, TaskCompletionSource<TValue?>)>();
|
||||
private Task? pendingRequestTask;
|
||||
private readonly object taskAssignmentLock = new object();
|
||||
|
||||
private Task<TValue> queryValue(TLookup id)
|
||||
private Task<TValue?> queryValue(TLookup id)
|
||||
{
|
||||
lock (taskAssignmentLock)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<TValue>();
|
||||
var tcs = new TaskCompletionSource<TValue?>();
|
||||
|
||||
// Add to the queue.
|
||||
pendingTasks.Enqueue((id, tcs));
|
||||
@@ -96,14 +91,14 @@ namespace osu.Game.Database
|
||||
private async Task performLookup()
|
||||
{
|
||||
// contains at most 50 unique IDs from tasks, which is used to perform the lookup.
|
||||
var nextTaskBatch = new Dictionary<TLookup, List<TaskCompletionSource<TValue>>>();
|
||||
var nextTaskBatch = new Dictionary<TLookup, List<TaskCompletionSource<TValue?>>>();
|
||||
|
||||
// Grab at most 50 unique IDs from the queue.
|
||||
lock (taskAssignmentLock)
|
||||
{
|
||||
while (pendingTasks.Count > 0 && nextTaskBatch.Count < 50)
|
||||
{
|
||||
(TLookup id, TaskCompletionSource<TValue> task) next = pendingTasks.Dequeue();
|
||||
(TLookup id, TaskCompletionSource<TValue?> task) next = pendingTasks.Dequeue();
|
||||
|
||||
// Perform a secondary check for existence, in case the value was queried in a previous batch.
|
||||
if (CheckExists(next.id, out var existing))
|
||||
@@ -113,7 +108,7 @@ namespace osu.Game.Database
|
||||
if (nextTaskBatch.TryGetValue(next.id, out var tasks))
|
||||
tasks.Add(next.task);
|
||||
else
|
||||
nextTaskBatch[next.id] = new List<TaskCompletionSource<TValue>> { next.task };
|
||||
nextTaskBatch[next.id] = new List<TaskCompletionSource<TValue?>> { next.task };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
|
||||
@@ -21,8 +18,7 @@ namespace osu.Game.Database
|
||||
/// <param name="userId">The user to lookup.</param>
|
||||
/// <param name="token">An optional cancellation token.</param>
|
||||
/// <returns>The populated user, or null if the user does not exist or the request could not be satisfied.</returns>
|
||||
[ItemCanBeNull]
|
||||
public Task<APIUser> GetUserAsync(int userId, CancellationToken token = default) => LookupAsync(userId, token);
|
||||
public Task<APIUser?> GetUserAsync(int userId, CancellationToken token = default) => LookupAsync(userId, token);
|
||||
|
||||
/// <summary>
|
||||
/// Perform an API lookup on the specified users, populating a <see cref="APIUser"/> model.
|
||||
@@ -30,10 +26,10 @@ namespace osu.Game.Database
|
||||
/// <param name="userIds">The users to lookup.</param>
|
||||
/// <param name="token">An optional cancellation token.</param>
|
||||
/// <returns>The populated users. May include null results for failed retrievals.</returns>
|
||||
public Task<APIUser[]> GetUsersAsync(int[] userIds, CancellationToken token = default) => LookupAsync(userIds, token);
|
||||
public Task<APIUser?[]> GetUsersAsync(int[] userIds, CancellationToken token = default) => LookupAsync(userIds, token);
|
||||
|
||||
protected override GetUsersRequest CreateRequest(IEnumerable<int> ids) => new GetUsersRequest(ids.ToArray());
|
||||
|
||||
protected override IEnumerable<APIUser> RetrieveResults(GetUsersRequest request) => request.Response?.Users;
|
||||
protected override IEnumerable<APIUser>? RetrieveResults(GetUsersRequest request) => request.Response?.Users;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Platform;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Extensions
|
||||
@@ -43,5 +44,20 @@ namespace osu.Game.Extensions
|
||||
/// <returns>The delta vector in Parent's coordinates.</returns>
|
||||
public static Vector2 ScreenSpaceDeltaToParentSpace(this Drawable drawable, Vector2 delta) =>
|
||||
drawable.Parent.ToLocalSpace(drawable.Parent.ToScreenSpace(Vector2.Zero) + delta);
|
||||
|
||||
/// <summary>
|
||||
/// Some elements don't handle rewind correctly and fixing them is non-trivial.
|
||||
/// In the future we need a better solution to this, but as a temporary work-around, give these components the game-wide
|
||||
/// clock so they don't need to worry about rewind.
|
||||
///
|
||||
/// This only works if input handling components handle OnPressed/OnReleased which results in a correct state while rewinding.
|
||||
///
|
||||
/// This is kinda dodgy (and will cause weirdness when pausing gameplay) but is better than completely broken rewind.
|
||||
/// </summary>
|
||||
public static void ApplyGameWideClock(this Drawable drawable, GameHost host)
|
||||
{
|
||||
drawable.Clock = host.UpdateThread.Clock;
|
||||
drawable.ProcessCustomClock = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,12 @@ namespace osu.Game.Extensions
|
||||
/// This is required as enum member names are not allowed to contain hyphens.
|
||||
/// </remarks>
|
||||
public static string ToCultureCode(this Language language)
|
||||
=> language.ToString().Replace("_", "-");
|
||||
{
|
||||
if (language == Language.zh_hant)
|
||||
return @"zh-tw";
|
||||
|
||||
return language.ToString().Replace("_", "-");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse the supplied <paramref name="cultureCode"/> to a <see cref="Language"/> value.
|
||||
@@ -30,7 +35,15 @@ namespace osu.Game.Extensions
|
||||
/// <param name="language">The parsed <see cref="Language"/>. Valid only if the return value of the method is <see langword="true" />.</param>
|
||||
/// <returns>Whether the parsing succeeded.</returns>
|
||||
public static bool TryParseCultureCode(string cultureCode, out Language language)
|
||||
=> Enum.TryParse(cultureCode.Replace("-", "_"), out language);
|
||||
{
|
||||
if (cultureCode == @"zh-tw")
|
||||
{
|
||||
language = Language.zh_hant;
|
||||
return true;
|
||||
}
|
||||
|
||||
return Enum.TryParse(cultureCode.Replace("-", "_"), out language);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses the <see cref="Language"/> that is specified in <paramref name="frameworkLocale"/>,
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(LargeTextureStore textures)
|
||||
{
|
||||
Sprite.Texture = Beatmap?.Background ?? textures.Get(fallbackTextureName);
|
||||
Sprite.Texture = Beatmap?.GetBackground() ?? textures.Get(fallbackTextureName);
|
||||
}
|
||||
|
||||
public override bool Equals(Background other)
|
||||
|
||||
@@ -152,7 +152,6 @@ namespace osu.Game.Graphics.Containers
|
||||
|
||||
protected override void PopOut()
|
||||
{
|
||||
base.PopOut();
|
||||
previewTrackManager.StopAnyPlaying(this);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
// 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.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Layout;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Framework.Utils;
|
||||
|
||||
namespace osu.Game.Graphics.Containers
|
||||
@@ -23,11 +22,35 @@ namespace osu.Game.Graphics.Containers
|
||||
public partial class SectionsContainer<T> : Container<T>
|
||||
where T : Drawable
|
||||
{
|
||||
public Bindable<T> SelectedSection { get; } = new Bindable<T>();
|
||||
public Bindable<T?> SelectedSection { get; } = new Bindable<T?>();
|
||||
|
||||
private T lastClickedSection;
|
||||
private T? lastClickedSection;
|
||||
|
||||
public Drawable ExpandableHeader
|
||||
protected override Container<T> Content => scrollContentContainer;
|
||||
|
||||
private readonly UserTrackingScrollContainer scrollContainer;
|
||||
private readonly Container headerBackgroundContainer;
|
||||
private readonly MarginPadding originalSectionsMargin;
|
||||
|
||||
private Drawable? fixedHeader;
|
||||
|
||||
private Drawable? footer;
|
||||
private Drawable? headerBackground;
|
||||
|
||||
private FlowContainer<T> scrollContentContainer = null!;
|
||||
|
||||
private float? headerHeight, footerHeight;
|
||||
|
||||
private float? lastKnownScroll;
|
||||
|
||||
/// <summary>
|
||||
/// The percentage of the container to consider the centre-point for deciding the active section (and scrolling to a requested section).
|
||||
/// </summary>
|
||||
private const float scroll_y_centre = 0.1f;
|
||||
|
||||
private Drawable? expandableHeader;
|
||||
|
||||
public Drawable? ExpandableHeader
|
||||
{
|
||||
get => expandableHeader;
|
||||
set
|
||||
@@ -42,11 +65,12 @@ namespace osu.Game.Graphics.Containers
|
||||
if (value == null) return;
|
||||
|
||||
AddInternal(expandableHeader);
|
||||
|
||||
lastKnownScroll = null;
|
||||
}
|
||||
}
|
||||
|
||||
public Drawable FixedHeader
|
||||
public Drawable? FixedHeader
|
||||
{
|
||||
get => fixedHeader;
|
||||
set
|
||||
@@ -63,7 +87,7 @@ namespace osu.Game.Graphics.Containers
|
||||
}
|
||||
}
|
||||
|
||||
public Drawable Footer
|
||||
public Drawable? Footer
|
||||
{
|
||||
get => footer;
|
||||
set
|
||||
@@ -75,16 +99,17 @@ namespace osu.Game.Graphics.Containers
|
||||
|
||||
footer = value;
|
||||
|
||||
if (value == null) return;
|
||||
if (footer == null) return;
|
||||
|
||||
footer.Anchor |= Anchor.y2;
|
||||
footer.Origin |= Anchor.y2;
|
||||
|
||||
scrollContainer.Add(footer);
|
||||
lastKnownScroll = null;
|
||||
}
|
||||
}
|
||||
|
||||
public Drawable HeaderBackground
|
||||
public Drawable? HeaderBackground
|
||||
{
|
||||
get => headerBackground;
|
||||
set
|
||||
@@ -102,23 +127,6 @@ namespace osu.Game.Graphics.Containers
|
||||
}
|
||||
}
|
||||
|
||||
protected override Container<T> Content => scrollContentContainer;
|
||||
|
||||
private readonly UserTrackingScrollContainer scrollContainer;
|
||||
private readonly Container headerBackgroundContainer;
|
||||
private readonly MarginPadding originalSectionsMargin;
|
||||
private Drawable expandableHeader, fixedHeader, footer, headerBackground;
|
||||
private FlowContainer<T> scrollContentContainer;
|
||||
|
||||
private float? headerHeight, footerHeight;
|
||||
|
||||
private float? lastKnownScroll;
|
||||
|
||||
/// <summary>
|
||||
/// The percentage of the container to consider the centre-point for deciding the active section (and scrolling to a requested section).
|
||||
/// </summary>
|
||||
private const float scroll_y_centre = 0.1f;
|
||||
|
||||
public SectionsContainer()
|
||||
{
|
||||
AddRangeInternal(new Drawable[]
|
||||
@@ -150,31 +158,63 @@ namespace osu.Game.Graphics.Containers
|
||||
footerHeight = null;
|
||||
}
|
||||
|
||||
private ScheduledDelegate? scrollToTargetDelegate;
|
||||
|
||||
public void ScrollTo(Drawable target)
|
||||
{
|
||||
Logger.Log($"Scrolling to {target}..");
|
||||
|
||||
lastKnownScroll = null;
|
||||
|
||||
// implementation similar to ScrollIntoView but a bit more nuanced.
|
||||
float top = scrollContainer.GetChildPosInContent(target);
|
||||
float scrollTarget = getScrollTargetForDrawable(target);
|
||||
|
||||
float bottomScrollExtent = scrollContainer.ScrollableExtent;
|
||||
float scrollTarget = top - scrollContainer.DisplayableContent * scroll_y_centre;
|
||||
|
||||
if (scrollTarget > bottomScrollExtent)
|
||||
if (scrollTarget > scrollContainer.ScrollableExtent)
|
||||
scrollContainer.ScrollToEnd();
|
||||
else
|
||||
scrollContainer.ScrollTo(scrollTarget);
|
||||
|
||||
if (target is T section)
|
||||
lastClickedSection = section;
|
||||
|
||||
// Content may load in as a scroll occurs, changing the scroll target we need to aim for.
|
||||
// This scheduled operation ensures that we keep trying until actually arriving at the target.
|
||||
scrollToTargetDelegate?.Cancel();
|
||||
scrollToTargetDelegate = Scheduler.AddDelayed(() =>
|
||||
{
|
||||
if (scrollContainer.UserScrolling)
|
||||
{
|
||||
Logger.Log("Scroll operation interrupted by user scroll");
|
||||
scrollToTargetDelegate?.Cancel();
|
||||
scrollToTargetDelegate = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (Precision.AlmostEquals(scrollContainer.Current, scrollTarget, 1))
|
||||
{
|
||||
Logger.Log($"Finished scrolling to {target}!");
|
||||
scrollToTargetDelegate?.Cancel();
|
||||
scrollToTargetDelegate = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Precision.AlmostEquals(getScrollTargetForDrawable(target), scrollTarget, 1))
|
||||
{
|
||||
Logger.Log($"Reattempting scroll to {target} due to change in position");
|
||||
ScrollTo(target);
|
||||
}
|
||||
}, 50, true);
|
||||
}
|
||||
|
||||
private float getScrollTargetForDrawable(Drawable target)
|
||||
{
|
||||
// implementation similar to ScrollIntoView but a bit more nuanced.
|
||||
return scrollContainer.GetChildPosInContent(target) - scrollContainer.DisplayableContent * scroll_y_centre;
|
||||
}
|
||||
|
||||
public void ScrollToTop() => scrollContainer.ScrollTo(0);
|
||||
|
||||
[NotNull]
|
||||
protected virtual UserTrackingScrollContainer CreateScrollContainer() => new UserTrackingScrollContainer();
|
||||
|
||||
[NotNull]
|
||||
protected virtual FlowContainer<T> CreateScrollContentContainer() =>
|
||||
new FillFlowContainer<T>
|
||||
{
|
||||
|
||||
@@ -3,12 +3,19 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
|
||||
namespace osu.Game.Graphics.Sprites
|
||||
{
|
||||
public partial class OsuSpriteText : SpriteText
|
||||
{
|
||||
[Obsolete("Use TruncatingSpriteText instead.")]
|
||||
public new bool Truncate
|
||||
{
|
||||
set => throw new InvalidOperationException($"Use {nameof(TruncatingSpriteText)} instead.");
|
||||
}
|
||||
|
||||
public OsuSpriteText()
|
||||
{
|
||||
Shadow = true;
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// 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.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Graphics.Sprites
|
||||
{
|
||||
/// <summary>
|
||||
/// A derived version of <see cref="OsuSpriteText"/> which automatically shows non-truncated text in tooltip when required.
|
||||
/// </summary>
|
||||
public sealed partial class TruncatingSpriteText : OsuSpriteText, IHasTooltip
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether a tooltip should be shown with non-truncated text on hover.
|
||||
/// </summary>
|
||||
public bool ShowTooltip { get; init; } = true;
|
||||
|
||||
public LocalisableString TooltipText => Text;
|
||||
|
||||
public override bool HandlePositionalInput => IsTruncated && ShowTooltip;
|
||||
|
||||
public TruncatingSpriteText()
|
||||
{
|
||||
((SpriteText)this).Truncate = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,9 +167,12 @@ namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
base.Update();
|
||||
|
||||
double elapsedDrawFrameTime = drawClock.ElapsedFrameTime;
|
||||
double elapsedUpdateFrameTime = updateClock.ElapsedFrameTime;
|
||||
|
||||
// If the game goes into a suspended state (ie. debugger attached or backgrounded on a mobile device)
|
||||
// we want to ignore really long periods of no processing.
|
||||
if (updateClock.ElapsedFrameTime > 10000)
|
||||
if (elapsedUpdateFrameTime > 10000)
|
||||
return;
|
||||
|
||||
mainContent.Width = Math.Max(mainContent.Width, counters.DrawWidth);
|
||||
@@ -178,17 +181,17 @@ namespace osu.Game.Graphics.UserInterface
|
||||
// frame limiter (we want to show the FPS as it's changing, even if it isn't an outlier).
|
||||
bool aimRatesChanged = updateAimFPS();
|
||||
|
||||
bool hasUpdateSpike = displayedFrameTime < spike_time_ms && updateClock.ElapsedFrameTime > spike_time_ms;
|
||||
bool hasUpdateSpike = displayedFrameTime < spike_time_ms && elapsedUpdateFrameTime > spike_time_ms;
|
||||
// use elapsed frame time rather then FramesPerSecond to better catch stutter frames.
|
||||
bool hasDrawSpike = displayedFpsCount > (1000 / spike_time_ms) && drawClock.ElapsedFrameTime > spike_time_ms;
|
||||
bool hasDrawSpike = displayedFpsCount > (1000 / spike_time_ms) && elapsedDrawFrameTime > spike_time_ms;
|
||||
|
||||
const float damp_time = 100;
|
||||
|
||||
displayedFrameTime = Interpolation.DampContinuously(displayedFrameTime, updateClock.ElapsedFrameTime, hasUpdateSpike ? 0 : damp_time, updateClock.ElapsedFrameTime);
|
||||
displayedFrameTime = Interpolation.DampContinuously(displayedFrameTime, elapsedUpdateFrameTime, hasUpdateSpike ? 0 : damp_time, elapsedUpdateFrameTime);
|
||||
|
||||
if (hasDrawSpike)
|
||||
// show spike time using raw elapsed value, to account for `FramesPerSecond` being so averaged spike frames don't show.
|
||||
displayedFpsCount = 1000 / drawClock.ElapsedFrameTime;
|
||||
displayedFpsCount = 1000 / elapsedDrawFrameTime;
|
||||
else
|
||||
displayedFpsCount = Interpolation.DampContinuously(displayedFpsCount, drawClock.FramesPerSecond, damp_time, Time.Elapsed);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
public enum MenuItemType
|
||||
|
||||
@@ -335,12 +335,11 @@ namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
Text = new OsuSpriteText
|
||||
Text = new TruncatingSpriteText
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Truncate = true,
|
||||
},
|
||||
Icon = new SpriteIcon
|
||||
{
|
||||
|
||||
@@ -16,6 +16,7 @@ using osu.Framework.Input;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Localisation;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
@@ -112,7 +113,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
private partial class CapsWarning : SpriteIcon, IHasTooltip
|
||||
{
|
||||
public LocalisableString TooltipText => "caps lock is active";
|
||||
public LocalisableString TooltipText => CommonStrings.CapsLockIsActive;
|
||||
|
||||
public CapsWarning()
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
public enum SelectionState
|
||||
|
||||
@@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Mods;
|
||||
@@ -37,6 +38,14 @@ namespace osu.Game.Graphics.UserInterface
|
||||
set => textBox.HoldFocus = value;
|
||||
}
|
||||
|
||||
public LocalisableString PlaceholderText
|
||||
{
|
||||
get => textBox.PlaceholderText;
|
||||
set => textBox.PlaceholderText = value;
|
||||
}
|
||||
|
||||
public new bool HasFocus => textBox.HasFocus;
|
||||
|
||||
public void TakeFocus() => textBox.TakeFocus();
|
||||
|
||||
public void KillFocus() => textBox.KillFocus();
|
||||
|
||||
@@ -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
|
||||
|
||||
namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user