mirror of
https://github.com/ppy/osu.git
synced 2024-12-13 03:42:57 +08:00
Merge branch 'master' into tournament-chatcolor
This commit is contained in:
commit
c5771912db
@ -15,7 +15,7 @@
|
||||
]
|
||||
},
|
||||
"codefilesanity": {
|
||||
"version": "0.0.36",
|
||||
"version": "0.0.37",
|
||||
"commands": [
|
||||
"CodeFileSanity"
|
||||
]
|
||||
|
@ -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);
|
||||
|
@ -2,7 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
/// <summary>
|
||||
/// A combo counter implementation that visually behaves almost similar to stable's osu!catch combo counter.
|
||||
/// </summary>
|
||||
public partial class LegacyCatchComboCounter : CompositeDrawable, ICatchComboCounter
|
||||
public partial class LegacyCatchComboCounter : UprightAspectMaintainingContainer, ICatchComboCounter
|
||||
{
|
||||
private readonly LegacyRollingCounter counter;
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
55
osu.Game.Rulesets.Osu/Mods/OsuModSynesthesia.cs
Normal file
55
osu.Game.Rulesets.Osu/Mods/OsuModSynesthesia.cs
Normal file
@ -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;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -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:
|
||||
|
@ -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]
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -11,6 +11,7 @@ using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Comments;
|
||||
using osuTK;
|
||||
@ -25,6 +26,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
|
||||
private TestCommentEditor commentEditor = null!;
|
||||
private TestCancellableCommentEditor cancellableCommentEditor = null!;
|
||||
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
@ -96,12 +98,43 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddUntilStep("button is not loading", () => !commentEditor.IsSpinnerShown);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLoggingInAndOut()
|
||||
{
|
||||
void assertLoggedInState()
|
||||
{
|
||||
AddAssert("commit button visible", () => commentEditor.ButtonsContainer[0].Alpha == 1);
|
||||
AddAssert("login button hidden", () => commentEditor.ButtonsContainer[1].Alpha == 0);
|
||||
AddAssert("text box editable", () => !commentEditor.TextBox.ReadOnly);
|
||||
}
|
||||
|
||||
void assertLoggedOutState()
|
||||
{
|
||||
AddAssert("commit button hidden", () => commentEditor.ButtonsContainer[0].Alpha == 0);
|
||||
AddAssert("login button visible", () => commentEditor.ButtonsContainer[1].Alpha == 1);
|
||||
AddAssert("text box readonly", () => commentEditor.TextBox.ReadOnly);
|
||||
}
|
||||
|
||||
// there's also the case of starting logged out, but more annoying to test.
|
||||
|
||||
// starting logged in
|
||||
assertLoggedInState();
|
||||
|
||||
// moving from logged in -> logged out
|
||||
AddStep("log out", () => dummyAPI.Logout());
|
||||
assertLoggedOutState();
|
||||
|
||||
// moving from logged out -> logged in
|
||||
AddStep("log back in", () => dummyAPI.Login("username", "password"));
|
||||
assertLoggedInState();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCancelAction()
|
||||
{
|
||||
AddStep("click cancel button", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(cancellableCommentEditor.ButtonsContainer[1]);
|
||||
InputManager.MoveMouseTo(cancellableCommentEditor.ButtonsContainer[2]);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
@ -112,6 +145,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
public new Bindable<string> Current => base.Current;
|
||||
public new FillFlowContainer ButtonsContainer => base.ButtonsContainer;
|
||||
public new TextBox TextBox => base.TextBox;
|
||||
|
||||
public string CommittedText { get; private set; } = string.Empty;
|
||||
|
||||
@ -125,8 +159,12 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
}
|
||||
|
||||
protected override LocalisableString FooterText => @"Footer text. And it is pretty long. Cool.";
|
||||
protected override LocalisableString CommitButtonText => @"Commit";
|
||||
protected override LocalisableString TextBoxPlaceholder => @"This text box is empty";
|
||||
|
||||
protected override LocalisableString GetButtonText(bool isLoggedIn) =>
|
||||
isLoggedIn ? @"Commit" : "You're logged out!";
|
||||
|
||||
protected override LocalisableString GetPlaceholderText(bool isLoggedIn) =>
|
||||
isLoggedIn ? @"This text box is empty" : "Still empty, but now you can't type in it.";
|
||||
}
|
||||
|
||||
private partial class TestCancellableCommentEditor : CancellableCommentEditor
|
||||
@ -146,8 +184,8 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
}
|
||||
|
||||
protected override LocalisableString CommitButtonText => @"Save";
|
||||
protected override LocalisableString TextBoxPlaceholder => @"Multiline textboxes soon";
|
||||
protected override LocalisableString GetButtonText(bool isLoggedIn) => @"Save";
|
||||
protected override LocalisableString GetPlaceholderText(bool isLoggedIn) => @"Multiline textboxes soon";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -42,5 +42,7 @@ namespace osu.Game.Tournament.Models
|
||||
};
|
||||
|
||||
public Bindable<bool> AutoProgressScreens = new BindableBool(true);
|
||||
|
||||
public Bindable<bool> SplitMapPoolByMods = new BindableBool(true);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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()
|
||||
},
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
@ -264,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);
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -76,8 +76,10 @@ namespace osu.Game.Database
|
||||
/// 26 2023-02-05 Added BeatmapHash to ScoreInfo.
|
||||
/// 27 2023-06-06 Added EditorTimestamp to BeatmapInfo.
|
||||
/// 28 2023-06-08 Added IsLegacyScore to ScoreInfo, parsed from replay files.
|
||||
/// 29 2023-06-12 Run migration of old lazer scores to be best-effort in the new scoring number space. No actual realm changes.
|
||||
/// 30 2023-06-16 Run migration of old lazer scores again. This time with more correct rounding considerations.
|
||||
/// </summary>
|
||||
private const int schema_version = 28;
|
||||
private const int schema_version = 30;
|
||||
|
||||
/// <summary>
|
||||
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
|
||||
@ -724,6 +726,11 @@ namespace osu.Game.Database
|
||||
|
||||
private void applyMigrationsForVersion(Migration migration, ulong targetVersion)
|
||||
{
|
||||
Logger.Log($"Running realm migration to version {targetVersion}...");
|
||||
Stopwatch stopwatch = new Stopwatch();
|
||||
|
||||
stopwatch.Start();
|
||||
|
||||
switch (targetVersion)
|
||||
{
|
||||
case 7:
|
||||
@ -930,7 +937,38 @@ namespace osu.Game.Database
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 29:
|
||||
case 30:
|
||||
{
|
||||
var scores = migration.NewRealm
|
||||
.All<ScoreInfo>()
|
||||
.Where(s => !s.IsLegacyScore);
|
||||
|
||||
foreach (var score in scores)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (StandardisedScoreMigrationTools.ShouldMigrateToNewStandardised(score))
|
||||
{
|
||||
try
|
||||
{
|
||||
long calculatedNew = StandardisedScoreMigrationTools.GetNewStandardised(score);
|
||||
score.TotalScore = calculatedNew;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms");
|
||||
}
|
||||
|
||||
private string? getRulesetShortNameFromLegacyID(long rulesetId)
|
||||
|
210
osu.Game/Database/StandardisedScoreMigrationTools.cs
Normal file
210
osu.Game/Database/StandardisedScoreMigrationTools.cs
Normal file
@ -0,0 +1,210 @@
|
||||
// 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.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
public static class StandardisedScoreMigrationTools
|
||||
{
|
||||
public static bool ShouldMigrateToNewStandardised(ScoreInfo score)
|
||||
{
|
||||
if (score.IsLegacyScore)
|
||||
return false;
|
||||
|
||||
// Recalculate the old-style standardised score to see if this was an old lazer score.
|
||||
bool oldScoreMatchesExpectations = GetOldStandardised(score) == score.TotalScore;
|
||||
// Some older scores don't have correct statistics populated, so let's give them benefit of doubt.
|
||||
bool scoreIsVeryOld = score.Date < new DateTime(2023, 1, 1, 0, 0, 0);
|
||||
|
||||
return oldScoreMatchesExpectations || scoreIsVeryOld;
|
||||
}
|
||||
|
||||
public static long GetNewStandardised(ScoreInfo score)
|
||||
{
|
||||
int maxJudgementIndex = 0;
|
||||
|
||||
// Avoid retrieving from realm inside loops.
|
||||
int maxCombo = score.MaxCombo;
|
||||
|
||||
var ruleset = score.Ruleset.CreateInstance();
|
||||
var processor = ruleset.CreateScoreProcessor();
|
||||
|
||||
processor.TrackHitEvents = false;
|
||||
|
||||
var beatmap = new Beatmap();
|
||||
|
||||
HitResult maxRulesetJudgement = ruleset.GetHitResults().First().result;
|
||||
|
||||
// This is a list of all results, ordered from best to worst.
|
||||
// We are constructing a "best possible" score from the statistics provided because it's the best we can do.
|
||||
List<HitResult> sortedHits = score.Statistics
|
||||
.Where(kvp => kvp.Key.AffectsCombo())
|
||||
.OrderByDescending(kvp => Judgement.ToNumericResult(kvp.Key))
|
||||
.SelectMany(kvp => Enumerable.Repeat(kvp.Key, kvp.Value))
|
||||
.ToList();
|
||||
|
||||
// Attempt to use maximum statistics from the database.
|
||||
var maximumJudgements = score.MaximumStatistics
|
||||
.Where(kvp => kvp.Key.AffectsCombo())
|
||||
.OrderByDescending(kvp => Judgement.ToNumericResult(kvp.Key))
|
||||
.SelectMany(kvp => Enumerable.Repeat(new FakeJudgement(kvp.Key), kvp.Value))
|
||||
.ToList();
|
||||
|
||||
// Some older scores may not have maximum statistics populated correctly.
|
||||
// In this case we need to fill them with best-known-defaults.
|
||||
if (maximumJudgements.Count != sortedHits.Count)
|
||||
{
|
||||
maximumJudgements = sortedHits
|
||||
.Select(r => new FakeJudgement(getMaxJudgementFor(r, maxRulesetJudgement)))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
// This is required to get the correct maximum combo portion.
|
||||
foreach (var judgement in maximumJudgements)
|
||||
beatmap.HitObjects.Add(new FakeHit(judgement));
|
||||
processor.ApplyBeatmap(beatmap);
|
||||
processor.Mods.Value = score.Mods;
|
||||
|
||||
// Insert all misses into a queue.
|
||||
// These will be nibbled at whenever we need to reset the combo.
|
||||
Queue<HitResult> misses = new Queue<HitResult>(score.Statistics
|
||||
.Where(kvp => kvp.Key == HitResult.Miss || kvp.Key == HitResult.LargeTickMiss)
|
||||
.SelectMany(kvp => Enumerable.Repeat(kvp.Key, kvp.Value)));
|
||||
|
||||
foreach (var result in sortedHits)
|
||||
{
|
||||
// For the main part of this loop, ignore all misses, as they will be inserted from the queue.
|
||||
if (result == HitResult.Miss || result == HitResult.LargeTickMiss)
|
||||
continue;
|
||||
|
||||
// Reset combo if required.
|
||||
if (processor.Combo.Value == maxCombo)
|
||||
insertMiss();
|
||||
|
||||
processor.ApplyResult(new JudgementResult(null!, maximumJudgements[maxJudgementIndex++])
|
||||
{
|
||||
Type = result
|
||||
});
|
||||
}
|
||||
|
||||
// Ensure we haven't forgotten any misses.
|
||||
while (misses.Count > 0)
|
||||
insertMiss();
|
||||
|
||||
var bonusHits = score.Statistics
|
||||
.Where(kvp => kvp.Key.IsBonus())
|
||||
.SelectMany(kvp => Enumerable.Repeat(kvp.Key, kvp.Value));
|
||||
|
||||
foreach (var result in bonusHits)
|
||||
processor.ApplyResult(new JudgementResult(null!, new FakeJudgement(result)) { Type = result });
|
||||
|
||||
// Not true for all scores for whatever reason. Oh well.
|
||||
// Debug.Assert(processor.HighestCombo.Value == score.MaxCombo);
|
||||
|
||||
return processor.TotalScore.Value;
|
||||
|
||||
void insertMiss()
|
||||
{
|
||||
if (misses.Count > 0)
|
||||
{
|
||||
processor.ApplyResult(new JudgementResult(null!, maximumJudgements[maxJudgementIndex++])
|
||||
{
|
||||
Type = misses.Dequeue(),
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// We ran out of misses. But we can't let max combo increase beyond the known value,
|
||||
// so let's forge a miss.
|
||||
processor.ApplyResult(new JudgementResult(null!, new FakeJudgement(getMaxJudgementFor(HitResult.Miss, maxRulesetJudgement)))
|
||||
{
|
||||
Type = HitResult.Miss,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static HitResult getMaxJudgementFor(HitResult hitResult, HitResult max)
|
||||
{
|
||||
switch (hitResult)
|
||||
{
|
||||
case HitResult.Miss:
|
||||
case HitResult.Meh:
|
||||
case HitResult.Ok:
|
||||
case HitResult.Good:
|
||||
case HitResult.Great:
|
||||
case HitResult.Perfect:
|
||||
return max;
|
||||
|
||||
case HitResult.SmallTickMiss:
|
||||
case HitResult.SmallTickHit:
|
||||
return HitResult.SmallTickHit;
|
||||
|
||||
case HitResult.LargeTickMiss:
|
||||
case HitResult.LargeTickHit:
|
||||
return HitResult.LargeTickHit;
|
||||
}
|
||||
|
||||
return HitResult.IgnoreHit;
|
||||
}
|
||||
|
||||
public static long GetOldStandardised(ScoreInfo score)
|
||||
{
|
||||
double accuracyScore =
|
||||
(double)score.Statistics.Where(kvp => kvp.Key.AffectsAccuracy()).Sum(kvp => Judgement.ToNumericResult(kvp.Key) * kvp.Value)
|
||||
/ score.MaximumStatistics.Where(kvp => kvp.Key.AffectsAccuracy()).Sum(kvp => Judgement.ToNumericResult(kvp.Key) * kvp.Value);
|
||||
double comboScore = (double)score.MaxCombo / score.MaximumStatistics.Where(kvp => kvp.Key.AffectsCombo()).Sum(kvp => kvp.Value);
|
||||
double bonusScore = score.Statistics.Where(kvp => kvp.Key.IsBonus()).Sum(kvp => Judgement.ToNumericResult(kvp.Key) * kvp.Value);
|
||||
|
||||
double accuracyPortion = 0.3;
|
||||
|
||||
switch (score.RulesetID)
|
||||
{
|
||||
case 1:
|
||||
accuracyPortion = 0.75;
|
||||
break;
|
||||
|
||||
case 3:
|
||||
accuracyPortion = 0.99;
|
||||
break;
|
||||
}
|
||||
|
||||
double modMultiplier = 1;
|
||||
|
||||
foreach (var mod in score.Mods)
|
||||
modMultiplier *= mod.ScoreMultiplier;
|
||||
|
||||
return (long)Math.Round((1000000 * (accuracyPortion * accuracyScore + (1 - accuracyPortion) * comboScore) + bonusScore) * modMultiplier);
|
||||
}
|
||||
|
||||
private class FakeHit : HitObject
|
||||
{
|
||||
private readonly Judgement judgement;
|
||||
|
||||
public override Judgement CreateJudgement() => judgement;
|
||||
|
||||
public FakeHit(Judgement judgement)
|
||||
{
|
||||
this.judgement = judgement;
|
||||
}
|
||||
}
|
||||
|
||||
private class FakeJudgement : Judgement
|
||||
{
|
||||
public override HitResult MaxResult { get; }
|
||||
|
||||
public FakeJudgement(HitResult maxResult)
|
||||
{
|
||||
MaxResult = maxResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -152,7 +152,6 @@ namespace osu.Game.Graphics.Containers
|
||||
|
||||
protected override void PopOut()
|
||||
{
|
||||
base.PopOut();
|
||||
previewTrackManager.StopAnyPlaying(this);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
29
osu.Game/Graphics/Sprites/TruncatingSpriteText.cs
Normal file
29
osu.Game/Graphics/Sprites/TruncatingSpriteText.cs
Normal file
@ -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);
|
||||
|
||||
|
@ -111,6 +111,10 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
// Handle case where a click is triggered via TriggerClick().
|
||||
if (!IsHovered)
|
||||
hover.FadeOutFromOne(1600);
|
||||
|
||||
hover.FlashColour(FlashColour, 800, Easing.OutQuint);
|
||||
return base.OnClick(e);
|
||||
}
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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();
|
||||
|
@ -119,6 +119,8 @@ namespace osu.Game.Input.Bindings
|
||||
new KeyBinding(InputKey.MouseMiddle, GlobalAction.PauseGameplay),
|
||||
new KeyBinding(InputKey.Control, GlobalAction.HoldForHUD),
|
||||
new KeyBinding(InputKey.Tab, GlobalAction.ToggleChatFocus),
|
||||
new KeyBinding(InputKey.F1, GlobalAction.SaveReplay),
|
||||
new KeyBinding(InputKey.F2, GlobalAction.ExportReplay),
|
||||
};
|
||||
|
||||
public IEnumerable<KeyBinding> ReplayKeyBindings => new[]
|
||||
@ -366,5 +368,11 @@ namespace osu.Game.Input.Bindings
|
||||
|
||||
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorCycleNextBeatSnapDivisor))]
|
||||
EditorCycleNextBeatSnapDivisor,
|
||||
|
||||
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.SaveReplay))]
|
||||
SaveReplay,
|
||||
|
||||
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ExportReplay))]
|
||||
ExportReplay,
|
||||
}
|
||||
}
|
||||
|
54
osu.Game/Localisation/AccountCreationStrings.cs
Normal file
54
osu.Game/Localisation/AccountCreationStrings.cs
Normal file
@ -0,0 +1,54 @@
|
||||
// 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.Localisation;
|
||||
|
||||
namespace osu.Game.Localisation
|
||||
{
|
||||
public static class AccountCreationStrings
|
||||
{
|
||||
private const string prefix = @"osu.Game.Resources.Localisation.AccountCreation";
|
||||
|
||||
/// <summary>
|
||||
/// "New player registration"
|
||||
/// </summary>
|
||||
public static LocalisableString NewPlayerRegistration => new TranslatableString(getKey(@"new_player_registration"), @"New player registration");
|
||||
|
||||
/// <summary>
|
||||
/// "Let's get you started"
|
||||
/// </summary>
|
||||
public static LocalisableString LetsGetYouStarted => new TranslatableString(getKey(@"lets_get_you_started"), @"Let's get you started");
|
||||
|
||||
/// <summary>
|
||||
/// "Let's create an account!"
|
||||
/// </summary>
|
||||
public static LocalisableString LetsCreateAnAccount => new TranslatableString(getKey(@"lets_create_an_account"), @"Let's create an account!");
|
||||
|
||||
/// <summary>
|
||||
/// "Help, I can't access my account!"
|
||||
/// </summary>
|
||||
public static LocalisableString MultiAccountWarningHelp => new TranslatableString(getKey(@"multi_account_warning_help"), @"Help, I can't access my account!");
|
||||
|
||||
/// <summary>
|
||||
/// "I understand. This account isn't for me."
|
||||
/// </summary>
|
||||
public static LocalisableString MultiAccountWarningAccept => new TranslatableString(getKey(@"multi_account_warning_accept"), @"I understand. This account isn't for me.");
|
||||
|
||||
/// <summary>
|
||||
/// "This will be your public presence. No profanity, no impersonation. Avoid exposing your own personal details, too!"
|
||||
/// </summary>
|
||||
public static LocalisableString UsernameDescription => new TranslatableString(getKey(@"username_description"), @"This will be your public presence. No profanity, no impersonation. Avoid exposing your own personal details, too!");
|
||||
|
||||
/// <summary>
|
||||
/// "Will be used for notifications, account verification and in the case you forget your password. No spam, ever."
|
||||
/// </summary>
|
||||
public static LocalisableString EmailDescription1 => new TranslatableString(getKey(@"email_description_1"), @"Will be used for notifications, account verification and in the case you forget your password. No spam, ever.");
|
||||
|
||||
/// <summary>
|
||||
/// " Make sure to get it right!"
|
||||
/// </summary>
|
||||
public static LocalisableString EmailDescription2 => new TranslatableString(getKey(@"email_description_2"), @" Make sure to get it right!");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
@ -154,6 +154,11 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString Exit => new TranslatableString(getKey(@"exit"), @"Exit");
|
||||
|
||||
/// <summary>
|
||||
/// "Caps lock is active"
|
||||
/// </summary>
|
||||
public static LocalisableString CapsLockIsActive => new TranslatableString(getKey(@"caps_lock_is_active"), @"Caps lock is active");
|
||||
|
||||
/// <summary>
|
||||
/// "Revert to default"
|
||||
/// </summary>
|
||||
|
@ -109,6 +109,11 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString RotationSnapped(float newRotation) => new TranslatableString(getKey(@"rotation_snapped"), @"{0:0}° (snapped)", newRotation);
|
||||
|
||||
/// <summary>
|
||||
/// "Limit distance snap placement to current time"
|
||||
/// </summary>
|
||||
public static LocalisableString LimitedDistanceSnap => new TranslatableString(getKey(@"limited_distance_snap_grid"), @"Limit distance snap placement to current time");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
@ -324,6 +324,16 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString ToggleChatFocus => new TranslatableString(getKey(@"toggle_chat_focus"), @"Toggle chat focus");
|
||||
|
||||
/// <summary>
|
||||
/// "Save replay"
|
||||
/// </summary>
|
||||
public static LocalisableString SaveReplay => new TranslatableString(getKey(@"save_replay"), @"Save replay");
|
||||
|
||||
/// <summary>
|
||||
/// "Export replay"
|
||||
/// </summary>
|
||||
public static LocalisableString ExportReplay => new TranslatableString(getKey(@"export_replay"), @"Export replay");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
49
osu.Game/Localisation/LoginPanelStrings.cs
Normal file
49
osu.Game/Localisation/LoginPanelStrings.cs
Normal file
@ -0,0 +1,49 @@
|
||||
// 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.Localisation;
|
||||
|
||||
namespace osu.Game.Localisation
|
||||
{
|
||||
public static class LoginPanelStrings
|
||||
{
|
||||
private const string prefix = @"osu.Game.Resources.Localisation.LoginPanel";
|
||||
|
||||
/// <summary>
|
||||
/// "Do not disturb"
|
||||
/// </summary>
|
||||
public static LocalisableString DoNotDisturb => new TranslatableString(getKey(@"do_not_disturb"), @"Do not disturb");
|
||||
|
||||
/// <summary>
|
||||
/// "Appear offline"
|
||||
/// </summary>
|
||||
public static LocalisableString AppearOffline => new TranslatableString(getKey(@"appear_offline"), @"Appear offline");
|
||||
|
||||
/// <summary>
|
||||
/// "Signed in"
|
||||
/// </summary>
|
||||
public static LocalisableString SignedIn => new TranslatableString(getKey(@"signed_in"), @"Signed in");
|
||||
|
||||
/// <summary>
|
||||
/// "Account"
|
||||
/// </summary>
|
||||
public static LocalisableString Account => new TranslatableString(getKey(@"account"), @"Account");
|
||||
|
||||
/// <summary>
|
||||
/// "Remember username"
|
||||
/// </summary>
|
||||
public static LocalisableString RememberUsername => new TranslatableString(getKey(@"remember_username"), @"Remember username");
|
||||
|
||||
/// <summary>
|
||||
/// "Stay signed in"
|
||||
/// </summary>
|
||||
public static LocalisableString StaySignedIn => new TranslatableString(getKey(@"stay_signed_in"), @"Stay signed in");
|
||||
|
||||
/// <summary>
|
||||
/// "Register"
|
||||
/// </summary>
|
||||
public static LocalisableString Register => new TranslatableString(getKey(@"register"), @"Register");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// 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.Localisation;
|
||||
@ -39,6 +39,11 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString UseCurrentMods => new TranslatableString(getKey(@"use_current_mods"), @"Use current mods");
|
||||
|
||||
/// <summary>
|
||||
/// "tab to search..."
|
||||
/// </summary>
|
||||
public static LocalisableString TabToSearch => new TranslatableString(getKey(@"tab_to_search"), @"tab to search...");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
}
|
@ -34,7 +34,8 @@ namespace osu.Game.Online.API
|
||||
|
||||
public string AccessToken => "token";
|
||||
|
||||
public bool IsLoggedIn => State.Value == APIState.Online;
|
||||
/// <seealso cref="APIAccess.IsLoggedIn"/>
|
||||
public bool IsLoggedIn => State.Value > APIState.Offline;
|
||||
|
||||
public string ProvidedUsername => LocalUser.Value.Username;
|
||||
|
||||
@ -114,8 +115,10 @@ namespace osu.Game.Online.API
|
||||
|
||||
public void Logout()
|
||||
{
|
||||
LocalUser.Value = new GuestUser();
|
||||
state.Value = APIState.Offline;
|
||||
// must happen after `state.Value` is changed such that subscribers to that bindable's value changes see the correct user.
|
||||
// compare: `APIAccess.Logout()`.
|
||||
LocalUser.Value = new GuestUser();
|
||||
}
|
||||
|
||||
public IHubClientConnector GetHubConnector(string clientName, string endpoint, bool preferMessagePack) => null;
|
||||
|
@ -63,6 +63,16 @@ namespace osu.Game.Online.API.Requests.Responses
|
||||
set => Length = TimeSpan.FromSeconds(value).TotalMilliseconds;
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public double HitLength { get; set; }
|
||||
|
||||
[JsonProperty(@"hit_length")]
|
||||
private double hitLengthInSeconds
|
||||
{
|
||||
get => TimeSpan.FromMilliseconds(HitLength).TotalSeconds;
|
||||
set => HitLength = TimeSpan.FromSeconds(value).TotalMilliseconds;
|
||||
}
|
||||
|
||||
[JsonProperty(@"convert")]
|
||||
public bool Convert { get; set; }
|
||||
|
||||
|
@ -6,6 +6,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@ -28,6 +29,7 @@ using osu.Framework.Input.Events;
|
||||
using osu.Framework.Input.Handlers.Tablet;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Beatmaps;
|
||||
@ -281,6 +283,52 @@ namespace osu.Game
|
||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) =>
|
||||
dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
|
||||
|
||||
private readonly List<string> dragDropFiles = new List<string>();
|
||||
private ScheduledDelegate dragDropImportSchedule;
|
||||
|
||||
public override void SetHost(GameHost host)
|
||||
{
|
||||
base.SetHost(host);
|
||||
|
||||
if (host.Window is SDL2Window sdlWindow)
|
||||
{
|
||||
sdlWindow.DragDrop += path =>
|
||||
{
|
||||
// on macOS/iOS, URL associations are handled via SDL_DROPFILE events.
|
||||
if (path.StartsWith(OSU_PROTOCOL, StringComparison.Ordinal))
|
||||
{
|
||||
HandleLink(path);
|
||||
return;
|
||||
}
|
||||
|
||||
lock (dragDropFiles)
|
||||
{
|
||||
dragDropFiles.Add(path);
|
||||
|
||||
Logger.Log($@"Adding ""{Path.GetFileName(path)}"" 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.
|
||||
dragDropImportSchedule?.Cancel();
|
||||
dragDropImportSchedule = Scheduler.AddDelayed(handlePendingDragDropImports, 100);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePendingDragDropImports()
|
||||
{
|
||||
lock (dragDropFiles)
|
||||
{
|
||||
Logger.Log($"Handling batch import of {dragDropFiles.Count} files");
|
||||
|
||||
string[] paths = dragDropFiles.ToArray();
|
||||
dragDropFiles.Clear();
|
||||
|
||||
Task.Factory.StartNew(() => Import(paths), TaskCreationOptions.LongRunning);
|
||||
}
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
|
@ -17,6 +17,7 @@ using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
@ -71,7 +72,7 @@ namespace osu.Game.Overlays.AccountCreation
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Font = OsuFont.GetFont(size: 20),
|
||||
Text = "Let's create an account!",
|
||||
Text = AccountCreationStrings.LetsCreateAnAccount
|
||||
},
|
||||
usernameTextBox = new OsuTextBox
|
||||
{
|
||||
@ -86,7 +87,7 @@ namespace osu.Game.Overlays.AccountCreation
|
||||
},
|
||||
emailTextBox = new OsuTextBox
|
||||
{
|
||||
PlaceholderText = "email address",
|
||||
PlaceholderText = ModelValidationStrings.UserAttributesUserEmail.ToLower(),
|
||||
RelativeSizeAxes = Axes.X,
|
||||
TabbableContentContainer = this
|
||||
},
|
||||
@ -118,7 +119,7 @@ namespace osu.Game.Overlays.AccountCreation
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Child = new SettingsButton
|
||||
{
|
||||
Text = "Register",
|
||||
Text = LoginPanelStrings.Register,
|
||||
Margin = new MarginPadding { Vertical = 20 },
|
||||
Action = performRegistration
|
||||
}
|
||||
@ -132,10 +133,10 @@ namespace osu.Game.Overlays.AccountCreation
|
||||
|
||||
textboxes = new[] { usernameTextBox, emailTextBox, passwordTextBox };
|
||||
|
||||
usernameDescription.AddText("This will be your public presence. No profanity, no impersonation. Avoid exposing your own personal details, too!");
|
||||
usernameDescription.AddText(AccountCreationStrings.UsernameDescription);
|
||||
|
||||
emailAddressDescription.AddText("Will be used for notifications, account verification and in the case you forget your password. No spam, ever.");
|
||||
emailAddressDescription.AddText(" Make sure to get it right!", cp => cp.Font = cp.Font.With(Typeface.Torus, weight: FontWeight.Bold));
|
||||
emailAddressDescription.AddText(AccountCreationStrings.EmailDescription1);
|
||||
emailAddressDescription.AddText(AccountCreationStrings.EmailDescription2, cp => cp.Font = cp.Font.With(Typeface.Torus, weight: FontWeight.Bold));
|
||||
|
||||
passwordDescription.AddText("At least ");
|
||||
characterCheckText = passwordDescription.AddText("8 characters long");
|
||||
|
@ -17,6 +17,7 @@ using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Screens.Menu;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osu.Game.Localisation;
|
||||
|
||||
namespace osu.Game.Overlays.AccountCreation
|
||||
{
|
||||
@ -101,13 +102,13 @@ namespace osu.Game.Overlays.AccountCreation
|
||||
},
|
||||
new SettingsButton
|
||||
{
|
||||
Text = "Help, I can't access my account!",
|
||||
Text = AccountCreationStrings.MultiAccountWarningHelp,
|
||||
Margin = new MarginPadding { Top = 50 },
|
||||
Action = () => game?.OpenUrlExternally(help_centre_url)
|
||||
},
|
||||
new DangerousSettingsButton
|
||||
{
|
||||
Text = "I understand. This account isn't for me.",
|
||||
Text = AccountCreationStrings.MultiAccountWarningAccept,
|
||||
Action = () => this.Push(new ScreenEntry())
|
||||
},
|
||||
furtherAssistance = new LinkFlowContainer(cp => cp.Font = cp.Font.With(size: 12))
|
||||
|
@ -1,9 +1,8 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Screens;
|
||||
@ -12,6 +11,7 @@ using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Screens.Menu;
|
||||
using osuTK;
|
||||
using osu.Game.Localisation;
|
||||
|
||||
namespace osu.Game.Overlays.AccountCreation
|
||||
{
|
||||
@ -46,18 +46,18 @@ namespace osu.Game.Overlays.AccountCreation
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Font = OsuFont.GetFont(size: 24, weight: FontWeight.Light),
|
||||
Text = "New Player Registration",
|
||||
Text = AccountCreationStrings.NewPlayerRegistration.ToTitle(),
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Font = OsuFont.GetFont(size: 12),
|
||||
Text = "let's get you started",
|
||||
Text = AccountCreationStrings.LetsGetYouStarted.ToLower(),
|
||||
},
|
||||
new SettingsButton
|
||||
{
|
||||
Text = "Let's create an account!",
|
||||
Text = AccountCreationStrings.LetsCreateAnAccount,
|
||||
Margin = new MarginPadding { Vertical = 120 },
|
||||
Action = () => this.Push(new ScreenWarning())
|
||||
}
|
||||
|
@ -90,7 +90,6 @@ namespace osu.Game.Overlays
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
base.PopIn();
|
||||
this.FadeIn(transition_time, Easing.OutQuint);
|
||||
|
||||
if (welcomeScreen.GetChildScreen() != null)
|
||||
|
@ -58,23 +58,25 @@ namespace osu.Game.Overlays.BeatmapSet
|
||||
|
||||
private void updateDisplay()
|
||||
{
|
||||
bpm.Value = BeatmapSet?.BPM.ToLocalisableString(@"0.##") ?? (LocalisableString)"-";
|
||||
|
||||
if (beatmapInfo == null)
|
||||
{
|
||||
bpm.Value = "-";
|
||||
|
||||
length.Value = string.Empty;
|
||||
circleCount.Value = string.Empty;
|
||||
sliderCount.Value = string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
length.TooltipText = BeatmapsetsStrings.ShowStatsTotalLength(TimeSpan.FromMilliseconds(beatmapInfo.Length).ToFormattedDuration());
|
||||
bpm.Value = beatmapInfo.BPM.ToLocalisableString(@"0.##");
|
||||
|
||||
length.Value = TimeSpan.FromMilliseconds(beatmapInfo.Length).ToFormattedDuration();
|
||||
|
||||
var onlineInfo = beatmapInfo as IBeatmapOnlineInfo;
|
||||
if (beatmapInfo is not IBeatmapOnlineInfo onlineInfo) return;
|
||||
|
||||
circleCount.Value = (onlineInfo?.CircleCount ?? 0).ToLocalisableString(@"N0");
|
||||
sliderCount.Value = (onlineInfo?.SliderCount ?? 0).ToLocalisableString(@"N0");
|
||||
circleCount.Value = onlineInfo.CircleCount.ToLocalisableString(@"N0");
|
||||
sliderCount.Value = onlineInfo.SliderCount.ToLocalisableString(@"N0");
|
||||
length.TooltipText = BeatmapsetsStrings.ShowStatsTotalLength(TimeSpan.FromMilliseconds(onlineInfo.HitLength).ToFormattedDuration());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -47,9 +47,6 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private ScoreManager scoreManager { get; set; }
|
||||
|
||||
private GetScoresRequest getScoresRequest;
|
||||
|
||||
private CancellationTokenSource loadCancellationSource;
|
||||
@ -85,7 +82,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
|
||||
MD5Hash = apiBeatmap.MD5Hash
|
||||
};
|
||||
|
||||
var scores = scoreManager.OrderByTotalScore(value.Scores.Select(s => s.ToScoreInfo(rulesets, beatmapInfo))).ToArray();
|
||||
var scores = value.Scores.Select(s => s.ToScoreInfo(rulesets, beatmapInfo)).OrderByTotalScore().ToArray();
|
||||
var topScore = scores.First();
|
||||
|
||||
scoreTable.DisplayScores(scores, apiBeatmap.Status.GrantsPerformancePoints());
|
||||
|
@ -85,7 +85,7 @@ namespace osu.Game.Overlays.Chat.ChannelList
|
||||
new Drawable?[]
|
||||
{
|
||||
createIcon(),
|
||||
text = new OsuSpriteText
|
||||
text = new TruncatingSpriteText
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
@ -94,7 +94,6 @@ namespace osu.Game.Overlays.Chat.ChannelList
|
||||
Colour = colourProvider.Light3,
|
||||
Margin = new MarginPadding { Bottom = 2 },
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Truncate = true,
|
||||
},
|
||||
createMentionPill(),
|
||||
close = createCloseButton(),
|
||||
|
@ -73,14 +73,13 @@ namespace osu.Game.Overlays.Chat
|
||||
Width = chatting_text_width,
|
||||
Masking = true,
|
||||
Padding = new MarginPadding { Horizontal = padding },
|
||||
Child = chattingText = new OsuSpriteText
|
||||
Child = chattingText = new TruncatingSpriteText
|
||||
{
|
||||
MaxWidth = chatting_text_width - padding * 2,
|
||||
Font = OsuFont.Torus.With(size: 20),
|
||||
Colour = colourProvider.Background1,
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
Truncate = true,
|
||||
},
|
||||
},
|
||||
searchIconContainer = new Container
|
||||
|
@ -92,11 +92,9 @@ namespace osu.Game.Overlays.Chat
|
||||
|
||||
Action = openUserProfile;
|
||||
|
||||
drawableText = new OsuSpriteText
|
||||
drawableText = new TruncatingSpriteText
|
||||
{
|
||||
Shadow = false,
|
||||
Truncate = true,
|
||||
EllipsisString = "…",
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
};
|
||||
|
@ -276,8 +276,6 @@ namespace osu.Game.Overlays
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
base.PopIn();
|
||||
|
||||
this.MoveToY(0, transition_length, Easing.OutQuint);
|
||||
this.FadeIn(transition_length, Easing.OutQuint);
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Online.API;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@ -24,19 +25,37 @@ namespace osu.Game.Overlays.Comments
|
||||
|
||||
protected abstract LocalisableString FooterText { get; }
|
||||
|
||||
protected abstract LocalisableString CommitButtonText { get; }
|
||||
|
||||
protected abstract LocalisableString TextBoxPlaceholder { get; }
|
||||
|
||||
protected FillFlowContainer ButtonsContainer { get; private set; } = null!;
|
||||
|
||||
protected readonly Bindable<string> Current = new Bindable<string>(string.Empty);
|
||||
|
||||
private RoundedButton commitButton = null!;
|
||||
private RoundedButton logInButton = null!;
|
||||
private LoadingSpinner loadingSpinner = null!;
|
||||
|
||||
protected TextBox TextBox { get; private set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
protected IAPIProvider API { get; private set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private LoginOverlay? loginOverlay { get; set; }
|
||||
|
||||
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the text content of the main action button.
|
||||
/// When <paramref name="isLoggedIn"/> is <see langword="true"/>, the text will apply to a button that posts a comment.
|
||||
/// When <paramref name="isLoggedIn"/> is <see langword="false"/>, the text will apply to a button that directs the user to the login overlay.
|
||||
/// </summary>
|
||||
protected abstract LocalisableString GetButtonText(bool isLoggedIn);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the placeholder text for the comment box.
|
||||
/// </summary>
|
||||
/// <param name="isLoggedIn">Whether the current user is logged in.</param>
|
||||
protected abstract LocalisableString GetPlaceholderText(bool isLoggedIn);
|
||||
|
||||
protected bool ShowLoadingSpinner
|
||||
{
|
||||
set
|
||||
@ -78,7 +97,6 @@ namespace osu.Game.Overlays.Comments
|
||||
{
|
||||
Height = 40,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
PlaceholderText = TextBoxPlaceholder,
|
||||
Current = Current
|
||||
},
|
||||
new Container
|
||||
@ -113,10 +131,19 @@ namespace osu.Game.Overlays.Comments
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(5, 0),
|
||||
Child = commitButton = new EditorButton
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Text = CommitButtonText,
|
||||
Action = () => OnCommit(Current.Value)
|
||||
commitButton = new EditorButton
|
||||
{
|
||||
Action = () => OnCommit(Current.Value),
|
||||
Text = GetButtonText(true)
|
||||
},
|
||||
logInButton = new EditorButton
|
||||
{
|
||||
Width = 100,
|
||||
Action = () => loginOverlay?.Show(),
|
||||
Text = GetButtonText(false)
|
||||
}
|
||||
}
|
||||
},
|
||||
loadingSpinner = new LoadingSpinner
|
||||
@ -134,12 +161,14 @@ namespace osu.Game.Overlays.Comments
|
||||
});
|
||||
|
||||
TextBox.OnCommit += (_, _) => commitButton.TriggerClick();
|
||||
apiState.BindTo(API.State);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
Current.BindValueChanged(_ => updateCommitButtonState(), true);
|
||||
apiState.BindValueChanged(updateStateForLoggedIn, true);
|
||||
}
|
||||
|
||||
protected abstract void OnCommit(string text);
|
||||
@ -147,6 +176,25 @@ namespace osu.Game.Overlays.Comments
|
||||
private void updateCommitButtonState() =>
|
||||
commitButton.Enabled.Value = loadingSpinner.State.Value == Visibility.Hidden && !string.IsNullOrEmpty(Current.Value);
|
||||
|
||||
private void updateStateForLoggedIn(ValueChangedEvent<APIState> state) => Schedule(() =>
|
||||
{
|
||||
bool isAvailable = state.NewValue > APIState.Offline;
|
||||
|
||||
TextBox.PlaceholderText = GetPlaceholderText(isAvailable);
|
||||
TextBox.ReadOnly = !isAvailable;
|
||||
|
||||
if (isAvailable)
|
||||
{
|
||||
commitButton.Show();
|
||||
logInButton.Hide();
|
||||
}
|
||||
else
|
||||
{
|
||||
commitButton.Hide();
|
||||
logInButton.Show();
|
||||
}
|
||||
});
|
||||
|
||||
private partial class EditorTextBox : OsuTextBox
|
||||
{
|
||||
protected override float LeftRightPadding => side_padding;
|
||||
|
@ -405,17 +405,16 @@ namespace osu.Game.Overlays.Comments
|
||||
[Resolved]
|
||||
private CommentsContainer commentsContainer { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; }
|
||||
|
||||
public Action<CommentBundle> OnPost;
|
||||
|
||||
//TODO should match web, left empty due to no multiline support
|
||||
protected override LocalisableString FooterText => default;
|
||||
|
||||
protected override LocalisableString CommitButtonText => CommonStrings.ButtonsPost;
|
||||
protected override LocalisableString GetButtonText(bool isLoggedIn) =>
|
||||
isLoggedIn ? CommonStrings.ButtonsPost : CommentsStrings.GuestButtonNew;
|
||||
|
||||
protected override LocalisableString TextBoxPlaceholder => CommentsStrings.PlaceholderNew;
|
||||
protected override LocalisableString GetPlaceholderText(bool isLoggedIn) =>
|
||||
isLoggedIn ? CommentsStrings.PlaceholderNew : AuthorizationStrings.RequireLogin;
|
||||
|
||||
protected override void OnCommit(string text)
|
||||
{
|
||||
@ -432,7 +431,7 @@ namespace osu.Game.Overlays.Comments
|
||||
Current.Value = string.Empty;
|
||||
OnPost?.Invoke(cb);
|
||||
});
|
||||
api.Queue(req);
|
||||
API.Queue(req);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
@ -18,16 +17,17 @@ namespace osu.Game.Overlays.Comments
|
||||
[Resolved]
|
||||
private CommentsContainer commentsContainer { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
private readonly Comment parentComment;
|
||||
|
||||
public Action<DrawableComment[]>? OnPost;
|
||||
|
||||
protected override LocalisableString FooterText => default;
|
||||
protected override LocalisableString CommitButtonText => CommonStrings.ButtonsReply;
|
||||
protected override LocalisableString TextBoxPlaceholder => CommentsStrings.PlaceholderReply;
|
||||
|
||||
protected override LocalisableString GetButtonText(bool isLoggedIn) =>
|
||||
isLoggedIn ? CommonStrings.ButtonsReply : CommentsStrings.GuestButtonReply;
|
||||
|
||||
protected override LocalisableString GetPlaceholderText(bool isLoggedIn) =>
|
||||
isLoggedIn ? CommentsStrings.PlaceholderReply : AuthorizationStrings.RequireLogin;
|
||||
|
||||
public ReplyCommentEditor(Comment parent)
|
||||
{
|
||||
@ -38,7 +38,8 @@ namespace osu.Game.Overlays.Comments
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
GetContainingInputManager().ChangeFocus(TextBox);
|
||||
if (!TextBox.ReadOnly)
|
||||
GetContainingInputManager().ChangeFocus(TextBox);
|
||||
}
|
||||
|
||||
protected override void OnCommit(string text)
|
||||
@ -51,7 +52,7 @@ namespace osu.Game.Overlays.Comments
|
||||
Logger.Error(e, "Posting reply comment failed.");
|
||||
});
|
||||
req.Success += cb => Schedule(processPostedComments, cb);
|
||||
api.Queue(req);
|
||||
API.Queue(req);
|
||||
}
|
||||
|
||||
private void processPostedComments(CommentBundle cb)
|
||||
|
@ -100,17 +100,15 @@ namespace osu.Game.Overlays.Dashboard.Home
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
new TruncatingSpriteText
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Truncate = true,
|
||||
Font = OsuFont.GetFont(weight: FontWeight.Regular),
|
||||
Text = BeatmapSet.Title
|
||||
},
|
||||
new OsuSpriteText
|
||||
new TruncatingSpriteText
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Truncate = true,
|
||||
Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular),
|
||||
Text = BeatmapSet.Artist
|
||||
},
|
||||
|
@ -99,7 +99,6 @@ namespace osu.Game.Overlays
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
base.PopIn();
|
||||
lowPassFilter.CutoffTo(300, 100, Easing.OutCubic);
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,7 @@ using osu.Game.Online.API;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osuTK;
|
||||
using osu.Game.Localisation;
|
||||
|
||||
namespace osu.Game.Overlays.Login
|
||||
{
|
||||
@ -47,7 +48,7 @@ namespace osu.Game.Overlays.Login
|
||||
RelativeSizeAxes = Axes.X;
|
||||
|
||||
ErrorTextFlowContainer errorText;
|
||||
LinkFlowContainer forgottenPaswordLink;
|
||||
LinkFlowContainer forgottenPasswordLink;
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
@ -71,15 +72,15 @@ namespace osu.Game.Overlays.Login
|
||||
},
|
||||
new SettingsCheckbox
|
||||
{
|
||||
LabelText = "Remember username",
|
||||
LabelText = LoginPanelStrings.RememberUsername,
|
||||
Current = config.GetBindable<bool>(OsuSetting.SaveUsername),
|
||||
},
|
||||
new SettingsCheckbox
|
||||
{
|
||||
LabelText = "Stay signed in",
|
||||
LabelText = LoginPanelStrings.StaySignedIn,
|
||||
Current = config.GetBindable<bool>(OsuSetting.SavePassword),
|
||||
},
|
||||
forgottenPaswordLink = new LinkFlowContainer
|
||||
forgottenPasswordLink = new LinkFlowContainer
|
||||
{
|
||||
Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS },
|
||||
RelativeSizeAxes = Axes.X,
|
||||
@ -105,7 +106,7 @@ namespace osu.Game.Overlays.Login
|
||||
},
|
||||
new SettingsButton
|
||||
{
|
||||
Text = "Register",
|
||||
Text = LoginPanelStrings.Register,
|
||||
Action = () =>
|
||||
{
|
||||
RequestHide?.Invoke();
|
||||
@ -114,7 +115,7 @@ namespace osu.Game.Overlays.Login
|
||||
}
|
||||
};
|
||||
|
||||
forgottenPaswordLink.AddLink(LayoutStrings.PopupLoginLoginForgot, $"{api.WebsiteRootUrl}/home/password-reset");
|
||||
forgottenPasswordLink.AddLink(LayoutStrings.PopupLoginLoginForgot, $"{api.WebsiteRootUrl}/home/password-reset");
|
||||
|
||||
password.OnCommit += (_, _) => performLogin();
|
||||
|
||||
|
@ -6,6 +6,7 @@
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input.Events;
|
||||
@ -81,7 +82,7 @@ namespace osu.Game.Overlays.Login
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = "ACCOUNT",
|
||||
Text = LoginPanelStrings.Account.ToUpper(),
|
||||
Margin = new MarginPadding { Bottom = 5 },
|
||||
Font = OsuFont.GetFont(weight: FontWeight.Bold),
|
||||
},
|
||||
@ -115,7 +116,7 @@ namespace osu.Game.Overlays.Login
|
||||
},
|
||||
};
|
||||
|
||||
linkFlow.AddLink("cancel", api.Logout, string.Empty);
|
||||
linkFlow.AddLink(Resources.Localisation.Web.CommonStrings.ButtonsCancel.ToLower(), api.Logout, string.Empty);
|
||||
break;
|
||||
|
||||
case APIState.Online:
|
||||
@ -140,7 +141,7 @@ namespace osu.Game.Overlays.Login
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Text = "Signed in",
|
||||
Text = LoginPanelStrings.SignedIn,
|
||||
Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold),
|
||||
Margin = new MarginPadding { Top = 5, Bottom = 5 },
|
||||
},
|
||||
|
@ -1,11 +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.ComponentModel;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Localisation;
|
||||
|
||||
namespace osu.Game.Overlays.Login
|
||||
{
|
||||
@ -14,13 +12,13 @@ namespace osu.Game.Overlays.Login
|
||||
[LocalisableDescription(typeof(UsersStrings), nameof(UsersStrings.StatusOnline))]
|
||||
Online,
|
||||
|
||||
[Description(@"Do not disturb")]
|
||||
[LocalisableDescription(typeof(LoginPanelStrings), nameof(LoginPanelStrings.DoNotDisturb))]
|
||||
DoNotDisturb,
|
||||
|
||||
[Description(@"Appear offline")]
|
||||
[LocalisableDescription(typeof(LoginPanelStrings), nameof(LoginPanelStrings.AppearOffline))]
|
||||
AppearOffline,
|
||||
|
||||
[Description(@"Sign out")]
|
||||
[LocalisableDescription(typeof(UserVerificationStrings), nameof(UserVerificationStrings.BoxInfoLogoutLink))]
|
||||
SignOut,
|
||||
}
|
||||
}
|
||||
|
@ -75,8 +75,6 @@ namespace osu.Game.Overlays
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
base.PopIn();
|
||||
|
||||
panel.Bounding = true;
|
||||
this.FadeIn(transition_time, Easing.OutQuint);
|
||||
|
||||
|
@ -246,9 +246,13 @@ namespace osu.Game.Overlays
|
||||
}
|
||||
}
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
this.FadeIn(200);
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
{
|
||||
base.PopOut();
|
||||
this.FadeOut(200);
|
||||
}
|
||||
|
||||
|
@ -6,16 +6,13 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
public partial class DeselectAllModsButton : ShearedButton, IKeyBindingHandler<GlobalAction>
|
||||
public partial class DeselectAllModsButton : ShearedButton
|
||||
{
|
||||
private readonly Bindable<IReadOnlyList<Mod>> selectedMods = new Bindable<IReadOnlyList<Mod>>();
|
||||
|
||||
@ -39,18 +36,5 @@ namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
Enabled.Value = selectedMods.Value.Any();
|
||||
}
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||
{
|
||||
if (e.Repeat || e.Action != GlobalAction.DeselectAllMods)
|
||||
return false;
|
||||
|
||||
TriggerClick();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ namespace osu.Game.Overlays.Mods.Input
|
||||
if (!mod_type_lookup.TryGetValue(e.Key, out var typesToMatch))
|
||||
return false;
|
||||
|
||||
var matchingMods = availableMods.Where(modState => matches(modState, typesToMatch) && !modState.Filtered.Value).ToArray();
|
||||
var matchingMods = availableMods.Where(modState => matches(modState, typesToMatch) && modState.Visible).ToArray();
|
||||
|
||||
if (matchingMods.Length == 0)
|
||||
return false;
|
||||
|
@ -48,7 +48,7 @@ namespace osu.Game.Overlays.Mods.Input
|
||||
if (index < 0)
|
||||
return false;
|
||||
|
||||
var modState = availableMods.Where(modState => !modState.Filtered.Value).ElementAtOrDefault(index);
|
||||
var modState = availableMods.Where(modState => modState.Visible).ElementAtOrDefault(index);
|
||||
if (modState == null)
|
||||
return false;
|
||||
|
||||
|
@ -46,7 +46,8 @@ namespace osu.Game.Overlays.Mods
|
||||
foreach (var mod in availableMods)
|
||||
{
|
||||
mod.Active.BindValueChanged(_ => updateState());
|
||||
mod.Filtered.BindValueChanged(_ => updateState());
|
||||
mod.MatchingTextFilter.BindValueChanged(_ => updateState());
|
||||
mod.ValidForSelection.BindValueChanged(_ => updateState());
|
||||
}
|
||||
|
||||
updateState();
|
||||
@ -145,12 +146,17 @@ namespace osu.Game.Overlays.Mods
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
Alpha = availableMods.All(mod => mod.Filtered.Value) ? 0 : 1;
|
||||
Alpha = availableMods.All(mod => !mod.Visible) ? 0 : 1;
|
||||
|
||||
if (toggleAllCheckbox != null && !SelectionAnimationRunning)
|
||||
{
|
||||
toggleAllCheckbox.Alpha = availableMods.Any(panel => !panel.Filtered.Value) ? 1 : 0;
|
||||
toggleAllCheckbox.Current.Value = availableMods.Where(panel => !panel.Filtered.Value).All(panel => panel.Active.Value);
|
||||
bool anyPanelsVisible = availableMods.Any(panel => panel.Visible);
|
||||
|
||||
toggleAllCheckbox.Alpha = anyPanelsVisible ? 1 : 0;
|
||||
|
||||
// checking `anyPanelsVisible` is important since `.All()` returns `true` for empty enumerables.
|
||||
if (anyPanelsVisible)
|
||||
toggleAllCheckbox.Current.Value = availableMods.Where(panel => panel.Visible).All(panel => panel.Active.Value);
|
||||
}
|
||||
}
|
||||
|
||||
@ -195,7 +201,7 @@ namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
pendingSelectionOperations.Clear();
|
||||
|
||||
foreach (var button in availableMods.Where(b => !b.Active.Value && !b.Filtered.Value))
|
||||
foreach (var button in availableMods.Where(b => !b.Active.Value && b.Visible))
|
||||
pendingSelectionOperations.Enqueue(() => button.Active.Value = true);
|
||||
}
|
||||
|
||||
@ -206,8 +212,13 @@ namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
pendingSelectionOperations.Clear();
|
||||
|
||||
foreach (var button in availableMods.Where(b => b.Active.Value && !b.Filtered.Value))
|
||||
pendingSelectionOperations.Enqueue(() => button.Active.Value = false);
|
||||
foreach (var button in availableMods.Where(b => b.Active.Value))
|
||||
{
|
||||
if (!button.Visible)
|
||||
button.Active.Value = false;
|
||||
else
|
||||
pendingSelectionOperations.Enqueue(() => button.Active.Value = false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -1,9 +1,12 @@
|
||||
// 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 osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.UI;
|
||||
@ -11,11 +14,10 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
public partial class ModPanel : ModSelectPanel
|
||||
public partial class ModPanel : ModSelectPanel, IFilterable
|
||||
{
|
||||
public Mod Mod => modState.Mod;
|
||||
public override BindableBool Active => modState.Active;
|
||||
public BindableBool Filtered => modState.Filtered;
|
||||
|
||||
protected override float IdleSwitchWidth => 54;
|
||||
protected override float ExpandedSwitchWidth => 70;
|
||||
@ -54,7 +56,8 @@ namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Filtered.BindValueChanged(_ => updateFilterState(), true);
|
||||
modState.ValidForSelection.BindValueChanged(_ => updateFilterState());
|
||||
modState.MatchingTextFilter.BindValueChanged(_ => updateFilterState(), true);
|
||||
}
|
||||
|
||||
protected override void Select()
|
||||
@ -71,9 +74,25 @@ namespace osu.Game.Overlays.Mods
|
||||
|
||||
#region Filtering support
|
||||
|
||||
/// <seealso cref="ModState.Visible"/>
|
||||
public bool Visible => modState.Visible;
|
||||
|
||||
public override IEnumerable<LocalisableString> FilterTerms => new[]
|
||||
{
|
||||
Mod.Name,
|
||||
Mod.Acronym,
|
||||
Mod.Description
|
||||
};
|
||||
|
||||
public override bool MatchingFilter
|
||||
{
|
||||
get => modState.MatchingTextFilter.Value;
|
||||
set => modState.MatchingTextFilter.Value = value;
|
||||
}
|
||||
|
||||
private void updateFilterState()
|
||||
{
|
||||
this.FadeTo(Filtered.Value ? 0 : 1);
|
||||
this.FadeTo(Visible ? 1 : 0);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
@ -9,6 +9,7 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics;
|
||||
@ -81,6 +82,27 @@ namespace osu.Game.Overlays.Mods
|
||||
Active.Value = new HashSet<Mod>(Preset.Value.Mods).SetEquals(selectedMods.Value);
|
||||
}
|
||||
|
||||
#region Filtering support
|
||||
|
||||
public override IEnumerable<LocalisableString> FilterTerms => getFilterTerms();
|
||||
|
||||
private IEnumerable<LocalisableString> getFilterTerms()
|
||||
{
|
||||
var preset = Preset.Value;
|
||||
|
||||
yield return preset.Name;
|
||||
yield return preset.Description;
|
||||
|
||||
foreach (Mod mod in preset.Mods)
|
||||
{
|
||||
yield return mod.Name;
|
||||
yield return mod.Acronym;
|
||||
yield return mod.Description;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IHasCustomTooltip
|
||||
|
||||
public ModPreset TooltipContent => Preset.Value;
|
||||
|
27
osu.Game/Overlays/Mods/ModSearchContainer.cs
Normal file
27
osu.Game/Overlays/Mods/ModSearchContainer.cs
Normal file
@ -0,0 +1,27 @@
|
||||
// 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.Containers;
|
||||
|
||||
namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
public partial class ModSearchContainer : SearchContainer
|
||||
{
|
||||
public new string SearchTerm
|
||||
{
|
||||
get => base.SearchTerm;
|
||||
set
|
||||
{
|
||||
if (value == SearchTerm)
|
||||
return;
|
||||
|
||||
base.SearchTerm = value;
|
||||
|
||||
// Manual filtering here is required because ModColumn can be hidden when search term applied,
|
||||
// causing the whole SearchContainer to become non-present and never actually perform a subsequent
|
||||
// filter.
|
||||
Filter();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -43,10 +43,15 @@ namespace osu.Game.Overlays.Mods
|
||||
/// </summary>
|
||||
public readonly Bindable<bool> Active = new BindableBool(true);
|
||||
|
||||
public string SearchTerm
|
||||
{
|
||||
set => ItemsFlow.SearchTerm = value;
|
||||
}
|
||||
|
||||
protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => base.ReceivePositionalInputAtSubTree(screenSpacePos) && Active.Value;
|
||||
|
||||
protected readonly Container ControlContainer;
|
||||
protected readonly FillFlowContainer ItemsFlow;
|
||||
protected readonly ModSearchContainer ItemsFlow;
|
||||
|
||||
private readonly TextFlowContainer headerText;
|
||||
private readonly Box headerBackground;
|
||||
@ -150,7 +155,7 @@ namespace osu.Game.Overlays.Mods
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
ClampExtension = 100,
|
||||
ScrollbarOverlapsContent = false,
|
||||
Child = ItemsFlow = new FillFlowContainer
|
||||
Child = ItemsFlow = new ModSearchContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
|
@ -12,6 +12,8 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Audio;
|
||||
@ -25,10 +27,11 @@ using osu.Game.Localisation;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Utils;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
public abstract partial class ModSelectOverlay : ShearedOverlayContainer, ISamplePlaybackDisabler
|
||||
public abstract partial class ModSelectOverlay : ShearedOverlayContainer, ISamplePlaybackDisabler, IKeyBindingHandler<PlatformAction>
|
||||
{
|
||||
public const int BUTTON_WIDTH = 200;
|
||||
|
||||
@ -64,6 +67,14 @@ namespace osu.Game.Overlays.Mods
|
||||
}
|
||||
}
|
||||
|
||||
public string SearchTerm
|
||||
{
|
||||
get => SearchTextBox.Current.Value;
|
||||
set => SearchTextBox.Current.Value = value;
|
||||
}
|
||||
|
||||
public ShearedSearchTextBox SearchTextBox { get; private set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the total score multiplier calculated from the current selected set of mods should be shown.
|
||||
/// </summary>
|
||||
@ -94,7 +105,7 @@ namespace osu.Game.Overlays.Mods
|
||||
};
|
||||
}
|
||||
|
||||
yield return new DeselectAllModsButton(this);
|
||||
yield return deselectAllModsButton = new DeselectAllModsButton(this);
|
||||
}
|
||||
|
||||
private readonly Bindable<Dictionary<ModType, IReadOnlyList<Mod>>> globalAvailableMods = new Bindable<Dictionary<ModType, IReadOnlyList<Mod>>>();
|
||||
@ -107,11 +118,14 @@ namespace osu.Game.Overlays.Mods
|
||||
private ColumnScrollContainer columnScroll = null!;
|
||||
private ColumnFlowContainer columnFlow = null!;
|
||||
private FillFlowContainer<ShearedButton> footerButtonFlow = null!;
|
||||
private DeselectAllModsButton deselectAllModsButton = null!;
|
||||
|
||||
private Container aboveColumnsContent = null!;
|
||||
private DifficultyMultiplierDisplay? multiplierDisplay;
|
||||
|
||||
protected ShearedButton BackButton { get; private set; } = null!;
|
||||
protected ShearedToggleButton? CustomisationButton { get; private set; }
|
||||
protected SelectAllModsButton? SelectAllModsButton { get; set; }
|
||||
|
||||
private Sample? columnAppearSample;
|
||||
|
||||
@ -146,6 +160,17 @@ namespace osu.Game.Overlays.Mods
|
||||
|
||||
MainAreaContent.AddRange(new Drawable[]
|
||||
{
|
||||
aboveColumnsContent = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = ModsEffectDisplay.HEIGHT,
|
||||
Padding = new MarginPadding { Horizontal = 100 },
|
||||
Child = SearchTextBox = new ShearedSearchTextBox
|
||||
{
|
||||
HoldFocus = false,
|
||||
Width = 300
|
||||
}
|
||||
},
|
||||
new OsuContextMenuContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
@ -153,7 +178,7 @@ namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
Padding = new MarginPadding
|
||||
{
|
||||
Top = (ShowTotalMultiplier ? ModsEffectDisplay.HEIGHT : 0) + PADDING,
|
||||
Top = ModsEffectDisplay.HEIGHT + PADDING,
|
||||
Bottom = PADDING
|
||||
},
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
@ -186,18 +211,10 @@ namespace osu.Game.Overlays.Mods
|
||||
|
||||
if (ShowTotalMultiplier)
|
||||
{
|
||||
MainAreaContent.Add(new Container
|
||||
aboveColumnsContent.Add(multiplierDisplay = new DifficultyMultiplierDisplay
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
AutoSizeAxes = Axes.X,
|
||||
Height = ModsEffectDisplay.HEIGHT,
|
||||
Margin = new MarginPadding { Horizontal = 100 },
|
||||
Child = multiplierDisplay = new DifficultyMultiplierDisplay
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre
|
||||
},
|
||||
Origin = Anchor.TopRight
|
||||
});
|
||||
}
|
||||
|
||||
@ -226,6 +243,14 @@ namespace osu.Game.Overlays.Mods
|
||||
globalAvailableMods.BindTo(game.AvailableMods);
|
||||
}
|
||||
|
||||
public override void Hide()
|
||||
{
|
||||
base.Hide();
|
||||
|
||||
// clear search for next user interaction with mod overlay
|
||||
SearchTextBox.Current.Value = string.Empty;
|
||||
}
|
||||
|
||||
private ModSettingChangeTracker? modSettingChangeTracker;
|
||||
|
||||
protected override void LoadComplete()
|
||||
@ -263,6 +288,12 @@ namespace osu.Game.Overlays.Mods
|
||||
|
||||
customisationVisible.BindValueChanged(_ => updateCustomisationVisualState(), true);
|
||||
|
||||
SearchTextBox.Current.BindValueChanged(query =>
|
||||
{
|
||||
foreach (var column in columnFlow.Columns)
|
||||
column.SearchTerm = query.NewValue;
|
||||
}, true);
|
||||
|
||||
// Start scrolled slightly to the right to give the user a sense that
|
||||
// there is more horizontal content available.
|
||||
ScheduleAfterChildren(() =>
|
||||
@ -272,6 +303,13 @@ namespace osu.Game.Overlays.Mods
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
SearchTextBox.PlaceholderText = SearchTextBox.HasFocus ? Resources.Localisation.Web.CommonStrings.InputSearch : ModSelectOverlayStrings.TabToSearch;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Select all visible mods in all columns.
|
||||
/// </summary>
|
||||
@ -344,7 +382,7 @@ namespace osu.Game.Overlays.Mods
|
||||
private void filterMods()
|
||||
{
|
||||
foreach (var modState in allAvailableMods)
|
||||
modState.Filtered.Value = !modState.Mod.HasImplementation || !IsValidMod.Invoke(modState.Mod);
|
||||
modState.ValidForSelection.Value = modState.Mod.HasImplementation && IsValidMod.Invoke(modState.Mod);
|
||||
}
|
||||
|
||||
private void updateMultiplier()
|
||||
@ -469,7 +507,7 @@ namespace osu.Game.Overlays.Mods
|
||||
|
||||
base.PopIn();
|
||||
|
||||
multiplierDisplay?
|
||||
aboveColumnsContent
|
||||
.FadeIn(fade_in_duration, Easing.OutQuint)
|
||||
.MoveToY(0, fade_in_duration, Easing.OutQuint);
|
||||
|
||||
@ -479,7 +517,7 @@ namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
var column = columnFlow[i].Column;
|
||||
|
||||
bool allFiltered = column is ModColumn modColumn && modColumn.AvailableMods.All(modState => modState.Filtered.Value);
|
||||
bool allFiltered = column is ModColumn modColumn && modColumn.AvailableMods.All(modState => !modState.Visible);
|
||||
|
||||
double delay = allFiltered ? 0 : nonFilteredColumnCount * 30;
|
||||
double duration = allFiltered ? 0 : fade_in_duration;
|
||||
@ -527,7 +565,7 @@ namespace osu.Game.Overlays.Mods
|
||||
|
||||
base.PopOut();
|
||||
|
||||
multiplierDisplay?
|
||||
aboveColumnsContent
|
||||
.FadeOut(fade_out_duration / 2, Easing.OutQuint)
|
||||
.MoveToY(-distance, fade_out_duration / 2, Easing.OutQuint);
|
||||
|
||||
@ -541,7 +579,7 @@ namespace osu.Game.Overlays.Mods
|
||||
|
||||
if (column is ModColumn modColumn)
|
||||
{
|
||||
allFiltered = modColumn.AvailableMods.All(modState => modState.Filtered.Value);
|
||||
allFiltered = modColumn.AvailableMods.All(modState => !modState.Visible);
|
||||
modColumn.FlushPendingSelections();
|
||||
}
|
||||
|
||||
@ -578,10 +616,38 @@ namespace osu.Game.Overlays.Mods
|
||||
// This is handled locally here because this overlay is being registered at the game level
|
||||
// and therefore takes away keyboard focus from the screen stack.
|
||||
case GlobalAction.ToggleModSelection:
|
||||
// Pressing toggle should completely hide the overlay in one shot.
|
||||
hideOverlay(true);
|
||||
return true;
|
||||
|
||||
// This is handled locally here due to conflicts in input handling between the search text box and the deselect all mods button.
|
||||
// Attempting to handle this action locally in both places leads to a possible scenario
|
||||
// wherein activating the binding will both change the contents of the search text box and deselect all mods.
|
||||
case GlobalAction.DeselectAllMods:
|
||||
{
|
||||
if (!SearchTextBox.HasFocus)
|
||||
{
|
||||
deselectAllModsButton.TriggerClick();
|
||||
return true;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case GlobalAction.Select:
|
||||
{
|
||||
// Pressing toggle or select should completely hide the overlay in one shot.
|
||||
hideOverlay(true);
|
||||
// Pressing select should select first filtered mod or completely hide the overlay in one shot if search term is empty.
|
||||
if (string.IsNullOrEmpty(SearchTerm))
|
||||
{
|
||||
hideOverlay(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
ModState? firstMod = columnFlow.Columns.OfType<ModColumn>().FirstOrDefault(m => m.IsPresent)?.AvailableMods.FirstOrDefault(x => x.Visible);
|
||||
|
||||
if (firstMod is not null)
|
||||
firstMod.Active.Value = !firstMod.Active.Value;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -603,6 +669,39 @@ namespace osu.Game.Overlays.Mods
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IKeyBindingHandler{PlatformAction}"/>
|
||||
/// <remarks>
|
||||
/// This is handled locally here due to conflicts in input handling between the search text box and the select all mods button.
|
||||
/// Attempting to handle this action locally in both places leads to a possible scenario
|
||||
/// wherein activating the "select all" platform binding will both select all text in the search box and select all mods.
|
||||
/// </remarks>>
|
||||
public bool OnPressed(KeyBindingPressEvent<PlatformAction> e)
|
||||
{
|
||||
if (e.Repeat || e.Action != PlatformAction.SelectAll || SelectAllModsButton is null)
|
||||
return false;
|
||||
|
||||
SelectAllModsButton.TriggerClick();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void OnReleased(KeyBindingReleaseEvent<PlatformAction> e)
|
||||
{
|
||||
}
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
if (e.Repeat || e.Key != Key.Tab)
|
||||
return false;
|
||||
|
||||
// TODO: should probably eventually support typical platform search shortcuts (`Ctrl-F`, `/`)
|
||||
if (SearchTextBox.HasFocus)
|
||||
SearchTextBox.KillFocus();
|
||||
else
|
||||
SearchTextBox.TakeFocus();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Sample playback control
|
||||
@ -743,6 +842,9 @@ namespace osu.Game.Overlays.Mods
|
||||
if (!Active.Value)
|
||||
RequestScroll?.Invoke(this);
|
||||
|
||||
// Killing focus is done here because it's the only feasible place on ModSelectOverlay you can click on without triggering any action.
|
||||
Scheduler.Add(() => GetContainingInputManager().ChangeFocus(null));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -1,6 +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 System.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
@ -25,7 +26,7 @@ using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
public abstract partial class ModSelectPanel : OsuClickableContainer, IHasAccentColour
|
||||
public abstract partial class ModSelectPanel : OsuClickableContainer, IHasAccentColour, IFilterable
|
||||
{
|
||||
public abstract BindableBool Active { get; }
|
||||
|
||||
@ -123,23 +124,23 @@ namespace osu.Game.Overlays.Mods
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new[]
|
||||
{
|
||||
titleText = new OsuSpriteText
|
||||
titleText = new TruncatingSpriteText
|
||||
{
|
||||
Font = OsuFont.TorusAlternate.With(size: 18, weight: FontWeight.SemiBold),
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Truncate = true,
|
||||
Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0),
|
||||
Margin = new MarginPadding
|
||||
{
|
||||
Left = -18 * ShearedOverlayContainer.SHEAR
|
||||
}
|
||||
},
|
||||
ShowTooltip = false, // Tooltip is handled by `IncompatibilityDisplayingModPanel`.
|
||||
},
|
||||
descriptionText = new OsuSpriteText
|
||||
descriptionText = new TruncatingSpriteText
|
||||
{
|
||||
Font = OsuFont.Default.With(size: 12),
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Truncate = true,
|
||||
Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0)
|
||||
Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0),
|
||||
ShowTooltip = false, // Tooltip is handled by `IncompatibilityDisplayingModPanel`.
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -199,6 +200,9 @@ namespace osu.Game.Overlays.Mods
|
||||
if (samplePlaybackDisabled.Value)
|
||||
return;
|
||||
|
||||
if (!IsPresent)
|
||||
return;
|
||||
|
||||
bool enoughTimePassedSinceLastPlayback = !lastPlaybackTime.Value.HasValue || Time.Current - lastPlaybackTime.Value >= SAMPLE_PLAYBACK_DELAY;
|
||||
|
||||
if (enoughTimePassedSinceLastPlayback)
|
||||
@ -277,5 +281,28 @@ namespace osu.Game.Overlays.Mods
|
||||
TextBackground.FadeColour(foregroundColour, transitionDuration, Easing.OutQuint);
|
||||
TextFlow.FadeColour(textColour, transitionDuration, Easing.OutQuint);
|
||||
}
|
||||
|
||||
#region IFilterable
|
||||
|
||||
public abstract IEnumerable<LocalisableString> FilterTerms { get; }
|
||||
|
||||
private bool matchingFilter = true;
|
||||
|
||||
public virtual bool MatchingFilter
|
||||
{
|
||||
get => matchingFilter;
|
||||
set
|
||||
{
|
||||
if (matchingFilter == value)
|
||||
return;
|
||||
|
||||
matchingFilter = value;
|
||||
this.FadeTo(value ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
public bool FilteringActive { set { } }
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
@ -32,9 +30,21 @@ namespace osu.Game.Overlays.Mods
|
||||
public bool PendingConfiguration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the mod is currently filtered out due to not matching imposed criteria.
|
||||
/// Whether the mod is currently valid for selection.
|
||||
/// This can be <see langword="false"/> in scenarios such as the free mod select overlay, where not all mods are selectable
|
||||
/// regardless of search criteria imposed by the user selecting.
|
||||
/// </summary>
|
||||
public BindableBool Filtered { get; } = new BindableBool();
|
||||
public BindableBool ValidForSelection { get; } = new BindableBool(true);
|
||||
|
||||
/// <summary>
|
||||
/// Whether the mod is matching the current textual filter.
|
||||
/// </summary>
|
||||
public BindableBool MatchingTextFilter { get; } = new BindableBool(true);
|
||||
|
||||
/// <summary>
|
||||
/// Whether the <see cref="Mod"/> matches all applicable filters and visible for the user to select.
|
||||
/// </summary>
|
||||
public bool Visible => MatchingTextFilter.Value && ValidForSelection.Value;
|
||||
|
||||
public ModState(Mod mod)
|
||||
{
|
||||
|
@ -1,14 +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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
@ -16,10 +11,11 @@ using osu.Game.Screens.OnlinePlay;
|
||||
|
||||
namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
public partial class SelectAllModsButton : ShearedButton, IKeyBindingHandler<PlatformAction>
|
||||
public partial class SelectAllModsButton : ShearedButton
|
||||
{
|
||||
private readonly Bindable<IReadOnlyList<Mod>> selectedMods = new Bindable<IReadOnlyList<Mod>>();
|
||||
private readonly Bindable<Dictionary<ModType, IReadOnlyList<ModState>>> availableMods = new Bindable<Dictionary<ModType, IReadOnlyList<ModState>>>();
|
||||
private readonly Bindable<string> searchTerm = new Bindable<string>();
|
||||
|
||||
public SelectAllModsButton(FreeModSelectOverlay modSelectOverlay)
|
||||
: base(ModSelectOverlay.BUTTON_WIDTH)
|
||||
@ -29,6 +25,7 @@ namespace osu.Game.Overlays.Mods
|
||||
|
||||
selectedMods.BindTo(modSelectOverlay.SelectedMods);
|
||||
availableMods.BindTo(modSelectOverlay.AvailableMods);
|
||||
searchTerm.BindTo(modSelectOverlay.SearchTextBox.Current);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@ -37,6 +34,7 @@ namespace osu.Game.Overlays.Mods
|
||||
|
||||
selectedMods.BindValueChanged(_ => Scheduler.AddOnce(updateEnabledState));
|
||||
availableMods.BindValueChanged(_ => Scheduler.AddOnce(updateEnabledState));
|
||||
searchTerm.BindValueChanged(_ => Scheduler.AddOnce(updateEnabledState));
|
||||
updateEnabledState();
|
||||
}
|
||||
|
||||
@ -44,20 +42,7 @@ namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
Enabled.Value = availableMods.Value
|
||||
.SelectMany(pair => pair.Value)
|
||||
.Any(modState => !modState.Active.Value && !modState.Filtered.Value);
|
||||
}
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<PlatformAction> e)
|
||||
{
|
||||
if (e.Repeat || e.Action != PlatformAction.SelectAll)
|
||||
return false;
|
||||
|
||||
TriggerClick();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void OnReleased(KeyBindingReleaseEvent<PlatformAction> e)
|
||||
{
|
||||
.Any(modState => !modState.Active.Value && modState.Visible);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -130,7 +130,6 @@ namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
const double fade_in_duration = 400;
|
||||
|
||||
base.PopIn();
|
||||
this.FadeIn(fade_in_duration, Easing.OutQuint);
|
||||
|
||||
Header.MoveToY(0, fade_in_duration, Easing.OutQuint);
|
||||
|
@ -316,6 +316,8 @@ namespace osu.Game.Overlays
|
||||
var queuedTrack = getQueuedTrack();
|
||||
|
||||
var lastTrack = CurrentTrack;
|
||||
lastTrack.Completed -= onTrackCompleted;
|
||||
|
||||
CurrentTrack = queuedTrack;
|
||||
|
||||
// At this point we may potentially be in an async context from tests. This is extremely dangerous but we have to make do for now.
|
||||
@ -344,16 +346,12 @@ namespace osu.Game.Overlays
|
||||
// Important to keep this in its own method to avoid inadvertently capturing unnecessary variables in the callback.
|
||||
// Can lead to leaks.
|
||||
var queuedTrack = new DrawableTrack(current.LoadTrack());
|
||||
queuedTrack.Completed += () => onTrackCompleted(current);
|
||||
queuedTrack.Completed += onTrackCompleted;
|
||||
return queuedTrack;
|
||||
}
|
||||
|
||||
private void onTrackCompleted(WorkingBeatmap workingBeatmap)
|
||||
private void onTrackCompleted()
|
||||
{
|
||||
// the source of track completion is the audio thread, so the beatmap may have changed before firing.
|
||||
if (current != workingBeatmap)
|
||||
return;
|
||||
|
||||
if (!CurrentTrack.Looping && !beatmap.Disabled)
|
||||
NextTrack();
|
||||
}
|
||||
|
@ -118,7 +118,7 @@ namespace osu.Game.Overlays
|
||||
|
||||
private void updateProcessingMode()
|
||||
{
|
||||
bool enabled = OverlayActivationMode.Value == OverlayActivation.All || State.Value == Visibility.Visible;
|
||||
bool enabled = OverlayActivationMode.Value != OverlayActivation.Disabled || State.Value == Visibility.Visible;
|
||||
|
||||
notificationsEnabler?.Cancel();
|
||||
|
||||
@ -206,8 +206,6 @@ namespace osu.Game.Overlays
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
base.PopIn();
|
||||
|
||||
this.MoveToX(0, TRANSITION_LENGTH, Easing.OutQuint);
|
||||
mainContent.FadeTo(1, TRANSITION_LENGTH, Easing.OutQuint);
|
||||
mainContent.FadeEdgeEffectTo(WaveContainer.SHADOW_OPACITY, WaveContainer.APPEAR_DURATION, Easing.Out);
|
||||
|
@ -229,8 +229,6 @@ namespace osu.Game.Overlays
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
base.PopIn();
|
||||
|
||||
this.FadeIn(transition_length, Easing.OutQuint);
|
||||
dragContainer.ScaleTo(1, transition_length, Easing.OutElastic);
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
|
||||
{
|
||||
protected override LocalisableString Header => AudioSettingsStrings.OffsetHeader;
|
||||
|
||||
public override IEnumerable<LocalisableString> FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { "universal", "uo", "timing" });
|
||||
public override IEnumerable<LocalisableString> FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { "universal", "uo", "timing", "delay", "latency" });
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config)
|
||||
|
@ -163,8 +163,6 @@ namespace osu.Game.Overlays
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
base.PopIn();
|
||||
|
||||
ContentContainer.MoveToX(ExpandedPosition, TRANSITION_LENGTH, Easing.OutQuint);
|
||||
|
||||
SectionsContainer.FadeEdgeEffectTo(WaveContainer.SHADOW_OPACITY, WaveContainer.APPEAR_DURATION, Easing.Out);
|
||||
|
@ -3,11 +3,13 @@
|
||||
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Screens;
|
||||
@ -45,6 +47,12 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
config.BindWith(OsuSetting.BeatmapSkins, beatmapSkins);
|
||||
}
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||
{
|
||||
switch (e.Action)
|
||||
@ -62,6 +70,8 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
globallyDisableBeatmapSkinSetting();
|
||||
|
||||
if (skinEditor != null)
|
||||
{
|
||||
skinEditor.Show();
|
||||
@ -87,7 +97,13 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
});
|
||||
}
|
||||
|
||||
protected override void PopOut() => skinEditor?.Hide();
|
||||
protected override void PopOut()
|
||||
{
|
||||
skinEditor?.Save(false);
|
||||
skinEditor?.Hide();
|
||||
|
||||
globallyReenableBeatmapSkinSetting();
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
@ -151,8 +167,6 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
|
||||
if (skinEditor == null) return;
|
||||
|
||||
skinEditor.Save(userTriggered: false);
|
||||
|
||||
// ensure the toolbar is re-hidden even if a new screen decides to try and show it.
|
||||
updateComponentVisibility();
|
||||
|
||||
@ -182,5 +196,25 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
skinEditor = null;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly Bindable<bool> beatmapSkins = new Bindable<bool>();
|
||||
private LeasedBindable<bool>? leasedBeatmapSkins;
|
||||
|
||||
private void globallyDisableBeatmapSkinSetting()
|
||||
{
|
||||
if (beatmapSkins.Disabled)
|
||||
return;
|
||||
|
||||
// The skin editor doesn't work well if beatmap skins are being applied to the player screen.
|
||||
// To keep things simple, disable the setting game-wide while using the skin editor.
|
||||
leasedBeatmapSkins = beatmapSkins.BeginLease(true);
|
||||
leasedBeatmapSkins.Value = false;
|
||||
}
|
||||
|
||||
private void globallyReenableBeatmapSkinSetting()
|
||||
{
|
||||
leasedBeatmapSkins?.Return();
|
||||
leasedBeatmapSkins = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -34,8 +34,6 @@ namespace osu.Game.Overlays
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
base.PopIn();
|
||||
|
||||
Waves.Show();
|
||||
this.FadeIn(100, Easing.OutQuint);
|
||||
}
|
||||
|
19
osu.Game/Rulesets/Mods/ModSynesthesia.cs
Normal file
19
osu.Game/Rulesets/Mods/ModSynesthesia.cs
Normal file
@ -0,0 +1,19 @@
|
||||
// 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.Localisation;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
/// <summary>
|
||||
/// Mod that colours hitobjects based on the musical division they are on
|
||||
/// </summary>
|
||||
public class ModSynesthesia : Mod
|
||||
{
|
||||
public override string Name => "Synesthesia";
|
||||
public override string Acronym => "SY";
|
||||
public override LocalisableString Description => "Colours hit objects based on the rhythm.";
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override ModType Type => ModType.Fun;
|
||||
}
|
||||
}
|
@ -30,6 +30,11 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// </summary>
|
||||
protected int MaxHits { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether <see cref="SimulateAutoplay"/> is currently running.
|
||||
/// </summary>
|
||||
protected bool IsSimulating { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The total number of judged <see cref="HitObject"/>s at the current point in time.
|
||||
/// </summary>
|
||||
@ -146,6 +151,8 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// <param name="beatmap">The <see cref="IBeatmap"/> to simulate.</param>
|
||||
protected virtual void SimulateAutoplay(IBeatmap beatmap)
|
||||
{
|
||||
IsSimulating = true;
|
||||
|
||||
foreach (var obj in beatmap.HitObjects)
|
||||
simulate(obj);
|
||||
|
||||
@ -163,6 +170,8 @@ namespace osu.Game.Rulesets.Scoring
|
||||
result.Type = GetSimulatedHitResult(judgement);
|
||||
ApplyResult(result);
|
||||
}
|
||||
|
||||
IsSimulating = false;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
|
@ -30,6 +30,14 @@ namespace osu.Game.Rulesets.Scoring
|
||||
private const double accuracy_cutoff_c = 0.7;
|
||||
private const double accuracy_cutoff_d = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Whether <see cref="HitEvents"/> should be populated during application of results.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Should only be disabled for special cases.
|
||||
/// When disabled, <see cref="JudgementProcessor.RevertResult"/> cannot be used.</remarks>
|
||||
internal bool TrackHitEvents = true;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when this <see cref="ScoreProcessor"/> was reset from a replay frame.
|
||||
/// </summary>
|
||||
@ -226,10 +234,16 @@ namespace osu.Game.Rulesets.Scoring
|
||||
|
||||
ApplyScoreChange(result);
|
||||
|
||||
hitEvents.Add(CreateHitEvent(result));
|
||||
lastHitObject = result.HitObject;
|
||||
if (!IsSimulating)
|
||||
{
|
||||
if (TrackHitEvents)
|
||||
{
|
||||
hitEvents.Add(CreateHitEvent(result));
|
||||
lastHitObject = result.HitObject;
|
||||
}
|
||||
|
||||
updateScore();
|
||||
updateScore();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -242,6 +256,9 @@ namespace osu.Game.Rulesets.Scoring
|
||||
|
||||
protected sealed override void RevertResultInternal(JudgementResult result)
|
||||
{
|
||||
if (!TrackHitEvents)
|
||||
throw new InvalidOperationException(@$"Rewind is not supported when {nameof(TrackHitEvents)} is disabled.");
|
||||
|
||||
Combo.Value = result.ComboAtJudgement;
|
||||
HighestCombo.Value = result.HighestComboAtJudgement;
|
||||
|
||||
@ -311,6 +328,9 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// <param name="storeResults">Whether to store the current state of the <see cref="ScoreProcessor"/> for future use.</param>
|
||||
protected override void Reset(bool storeResults)
|
||||
{
|
||||
// Run one last time to store max values.
|
||||
updateScore();
|
||||
|
||||
base.Reset(storeResults);
|
||||
|
||||
hitEvents.Clear();
|
||||
|
@ -83,6 +83,11 @@ namespace osu.Game.Scoring
|
||||
|
||||
if (string.IsNullOrEmpty(model.MaximumStatisticsJson))
|
||||
model.MaximumStatisticsJson = JsonConvert.SerializeObject(model.MaximumStatistics);
|
||||
|
||||
// for pre-ScoreV2 lazer scores, apply a best-effort conversion of total score to ScoreV2.
|
||||
// this requires: max combo, statistics, max statistics (where available), and mods to already be populated on the score.
|
||||
if (StandardisedScoreMigrationTools.ShouldMigrateToNewStandardised(model))
|
||||
model.TotalScore = StandardisedScoreMigrationTools.GetNewStandardised(model);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -1,9 +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 osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Scoring
|
||||
{
|
||||
@ -13,5 +14,23 @@ namespace osu.Game.Scoring
|
||||
/// A user-presentable display title representing this score.
|
||||
/// </summary>
|
||||
public static string GetDisplayTitle(this IScoreInfo scoreInfo) => $"{scoreInfo.User.Username} playing {scoreInfo.Beatmap.GetDisplayTitle()}";
|
||||
|
||||
/// <summary>
|
||||
/// Orders an array of <see cref="ScoreInfo"/>s by total score.
|
||||
/// </summary>
|
||||
/// <param name="scores">The array of <see cref="ScoreInfo"/>s to reorder.</param>
|
||||
/// <returns>The given <paramref name="scores"/> ordered by decreasing total score.</returns>
|
||||
public static IEnumerable<ScoreInfo> OrderByTotalScore(this IEnumerable<ScoreInfo> scores)
|
||||
=> scores.OrderByDescending(s => s.TotalScore)
|
||||
.ThenBy(s => s.OnlineID)
|
||||
// Local scores may not have an online ID. Fall back to date in these cases.
|
||||
.ThenBy(s => s.Date);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the maximum achievable combo for the provided score.
|
||||
/// </summary>
|
||||
/// <param name="score">The <see cref="ScoreInfo"/> to compute the maximum achievable combo for.</param>
|
||||
/// <returns>The maximum achievable combo.</returns>
|
||||
public static int GetMaximumAchievableCombo(this ScoreInfo score) => score.MaximumStatistics.Where(kvp => kvp.Key.AffectsCombo()).Sum(kvp => kvp.Value);
|
||||
}
|
||||
}
|
||||
|
@ -69,17 +69,6 @@ namespace osu.Game.Scoring
|
||||
return Realm.Run(r => r.All<ScoreInfo>().FirstOrDefault(query)?.Detach());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Orders an array of <see cref="ScoreInfo"/>s by total score.
|
||||
/// </summary>
|
||||
/// <param name="scores">The array of <see cref="ScoreInfo"/>s to reorder.</param>
|
||||
/// <returns>The given <paramref name="scores"/> ordered by decreasing total score.</returns>
|
||||
public IEnumerable<ScoreInfo> OrderByTotalScore(IEnumerable<ScoreInfo> scores)
|
||||
=> scores.OrderByDescending(s => s.TotalScore)
|
||||
.ThenBy(s => s.OnlineID)
|
||||
// Local scores may not have an online ID. Fall back to date in these cases.
|
||||
.ThenBy(s => s.Date);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a bindable that represents the total score of a <see cref="ScoreInfo"/>.
|
||||
/// </summary>
|
||||
@ -100,13 +89,6 @@ namespace osu.Game.Scoring
|
||||
/// <returns>The bindable containing the formatted total score string.</returns>
|
||||
public Bindable<string> GetBindableTotalScoreString([NotNull] ScoreInfo score) => new TotalScoreStringBindable(GetBindableTotalScore(score));
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the maximum achievable combo for the provided score.
|
||||
/// </summary>
|
||||
/// <param name="score">The <see cref="ScoreInfo"/> to compute the maximum achievable combo for.</param>
|
||||
/// <returns>The maximum achievable combo.</returns>
|
||||
public int GetMaximumAchievableCombo([NotNull] ScoreInfo score) => score.MaximumStatistics.Where(kvp => kvp.Key.AffectsCombo()).Sum(kvp => kvp.Value);
|
||||
|
||||
/// <summary>
|
||||
/// Provides the total score of a <see cref="ScoreInfo"/>. Responds to changes in the currently-selected <see cref="ScoringMode"/>.
|
||||
/// </summary>
|
||||
|
@ -86,7 +86,7 @@ namespace osu.Game.Screens.Backgrounds
|
||||
if (nextBackground == background)
|
||||
return false;
|
||||
|
||||
Logger.Log("🌅 Background change queued");
|
||||
Logger.Log(@"🌅 Global background change queued");
|
||||
|
||||
cancellationTokenSource?.Cancel();
|
||||
cancellationTokenSource = new CancellationTokenSource();
|
||||
@ -94,6 +94,7 @@ namespace osu.Game.Screens.Backgrounds
|
||||
nextTask?.Cancel();
|
||||
nextTask = Scheduler.AddDelayed(() =>
|
||||
{
|
||||
Logger.Log(@"🌅 Global background loading");
|
||||
LoadComponentAsync(nextBackground, displayNext, cancellationTokenSource.Token);
|
||||
}, 500);
|
||||
|
||||
|
@ -76,6 +76,9 @@ namespace osu.Game.Screens.Edit.Components
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
if (e.Repeat)
|
||||
return false;
|
||||
|
||||
switch (e.Key)
|
||||
{
|
||||
case Key.Space:
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user