1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-12 19:03:08 +08:00

Merge branch 'master' into tourney-save-changes-button-ux

This commit is contained in:
Salman Ahmed 2022-07-14 04:37:03 +03:00 committed by GitHub
commit 2734a28115
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 479 additions and 131 deletions

View File

@ -52,7 +52,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.702.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.707.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.713.0" />
</ItemGroup>
<ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->

View File

@ -0,0 +1,175 @@
// 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 NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Replays;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public class TestSceneOsuModSingleTap : OsuModTestScene
{
[Test]
public void TestInputSingular() => CreateModTest(new ModTestData
{
Mod = new OsuModSingleTap(),
PassCondition = () => Player.ScoreProcessor.Combo.Value == 2,
Autoplay = false,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new HitCircle
{
StartTime = 500,
Position = new Vector2(100),
},
new HitCircle
{
StartTime = 1000,
Position = new Vector2(200, 100),
},
new HitCircle
{
StartTime = 1500,
Position = new Vector2(300, 100),
},
new HitCircle
{
StartTime = 2000,
Position = new Vector2(400, 100),
},
},
},
ReplayFrames = new List<ReplayFrame>
{
new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton),
new OsuReplayFrame(501, new Vector2(100)),
new OsuReplayFrame(1000, new Vector2(200, 100), OsuAction.LeftButton),
}
});
[Test]
public void TestInputAlternating() => CreateModTest(new ModTestData
{
Mod = new OsuModSingleTap(),
PassCondition = () => Player.ScoreProcessor.Combo.Value == 0 && Player.ScoreProcessor.HighestCombo.Value == 1,
Autoplay = false,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new HitCircle
{
StartTime = 500,
Position = new Vector2(100),
},
new HitCircle
{
StartTime = 1000,
Position = new Vector2(200, 100),
},
},
},
ReplayFrames = new List<ReplayFrame>
{
new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton),
new OsuReplayFrame(501, new Vector2(100)),
new OsuReplayFrame(1000, new Vector2(200, 100), OsuAction.RightButton),
new OsuReplayFrame(1001, new Vector2(200, 100)),
new OsuReplayFrame(1500, new Vector2(300, 100), OsuAction.LeftButton),
new OsuReplayFrame(1501, new Vector2(300, 100)),
new OsuReplayFrame(2000, new Vector2(400, 100), OsuAction.RightButton),
new OsuReplayFrame(2001, new Vector2(400, 100)),
}
});
/// <summary>
/// Ensures singletapping is reset before the first hitobject after intro.
/// </summary>
[Test]
public void TestInputAlternatingAtIntro() => CreateModTest(new ModTestData
{
Mod = new OsuModSingleTap(),
PassCondition = () => Player.ScoreProcessor.Combo.Value == 1,
Autoplay = false,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new HitCircle
{
StartTime = 1000,
Position = new Vector2(100),
},
},
},
ReplayFrames = new List<ReplayFrame>
{
// first press during intro.
new OsuReplayFrame(500, new Vector2(200), OsuAction.LeftButton),
new OsuReplayFrame(501, new Vector2(200)),
// press different key at hitobject and ensure it has been hit.
new OsuReplayFrame(1000, new Vector2(100), OsuAction.RightButton),
}
});
/// <summary>
/// Ensures singletapping is reset before the first hitobject after a break.
/// </summary>
[Test]
public void TestInputAlternatingWithBreak() => CreateModTest(new ModTestData
{
Mod = new OsuModSingleTap(),
PassCondition = () => Player.ScoreProcessor.Combo.Value == 0 && Player.ScoreProcessor.HighestCombo.Value == 2,
Autoplay = false,
Beatmap = new Beatmap
{
Breaks = new List<BreakPeriod>
{
new BreakPeriod(500, 2000),
},
HitObjects = new List<HitObject>
{
new HitCircle
{
StartTime = 500,
Position = new Vector2(100),
},
new HitCircle
{
StartTime = 2500,
Position = new Vector2(500, 100),
},
new HitCircle
{
StartTime = 3000,
Position = new Vector2(500, 100),
},
}
},
ReplayFrames = new List<ReplayFrame>
{
// first press to start singletap lock.
new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton),
new OsuReplayFrame(501, new Vector2(100)),
// press different key after break but before hit object.
new OsuReplayFrame(2250, new Vector2(300, 100), OsuAction.RightButton),
new OsuReplayFrame(2251, new Vector2(300, 100)),
// press same key at second hitobject and ensure it has been hit.
new OsuReplayFrame(2500, new Vector2(500, 100), OsuAction.LeftButton),
new OsuReplayFrame(2501, new Vector2(500, 100)),
// press different key at third hitobject and ensure it has been missed.
new OsuReplayFrame(3000, new Vector2(500, 100), OsuAction.RightButton),
new OsuReplayFrame(3001, new Vector2(500, 100)),
}
});
}
}

View File

@ -0,0 +1,114 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
using osu.Game.Utils;
namespace osu.Game.Rulesets.Osu.Mods
{
public abstract class InputBlockingMod : Mod, IApplicableToDrawableRuleset<OsuHitObject>
{
public override double ScoreMultiplier => 1.0;
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModRelax), typeof(OsuModCinema) };
public override ModType Type => ModType.Conversion;
private const double flash_duration = 1000;
private DrawableRuleset<OsuHitObject> ruleset = null!;
protected OsuAction? LastAcceptedAction { get; private set; }
/// <summary>
/// A tracker for periods where alternate should not be forced (i.e. non-gameplay periods).
/// </summary>
/// <remarks>
/// This is different from <see cref="Player.IsBreakTime"/> in that the periods here end strictly at the first object after the break, rather than the break's end time.
/// </remarks>
private PeriodTracker nonGameplayPeriods = null!;
private IFrameStableClock gameplayClock = null!;
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{
ruleset = drawableRuleset;
drawableRuleset.KeyBindingInputManager.Add(new InputInterceptor(this));
var periods = new List<Period>();
if (drawableRuleset.Objects.Any())
{
periods.Add(new Period(int.MinValue, getValidJudgementTime(ruleset.Objects.First()) - 1));
foreach (BreakPeriod b in drawableRuleset.Beatmap.Breaks)
periods.Add(new Period(b.StartTime, getValidJudgementTime(ruleset.Objects.First(h => h.StartTime >= b.EndTime)) - 1));
static double getValidJudgementTime(HitObject hitObject) => hitObject.StartTime - hitObject.HitWindows.WindowFor(HitResult.Meh);
}
nonGameplayPeriods = new PeriodTracker(periods);
gameplayClock = drawableRuleset.FrameStableClock;
}
protected abstract bool CheckValidNewAction(OsuAction action);
private bool checkCorrectAction(OsuAction action)
{
if (nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime))
{
LastAcceptedAction = null;
return true;
}
switch (action)
{
case OsuAction.LeftButton:
case OsuAction.RightButton:
break;
// Any action which is not left or right button should be ignored.
default:
return true;
}
if (CheckValidNewAction(action))
{
LastAcceptedAction = action;
return true;
}
ruleset.Cursor.FlashColour(Colour4.Red, flash_duration, Easing.OutQuint);
return false;
}
private class InputInterceptor : Component, IKeyBindingHandler<OsuAction>
{
private readonly InputBlockingMod mod;
public InputInterceptor(InputBlockingMod mod)
{
this.mod = mod;
}
public bool OnPressed(KeyBindingPressEvent<OsuAction> e)
// if the pressed action is incorrect, block it from reaching gameplay.
=> !mod.checkCorrectAction(e.Action);
public void OnReleased(KeyBindingReleaseEvent<OsuAction> e)
{
}
}
}
}

View File

@ -1,119 +1,20 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
using osu.Game.Utils;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModAlternate : Mod, IApplicableToDrawableRuleset<OsuHitObject>
public class OsuModAlternate : InputBlockingMod
{
public override string Name => @"Alternate";
public override string Acronym => @"AL";
public override string Description => @"Don't use the same key twice in a row!";
public override double ScoreMultiplier => 1.0;
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModRelax) };
public override ModType Type => ModType.Conversion;
public override IconUsage? Icon => FontAwesome.Solid.Keyboard;
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModSingleTap) }).ToArray();
private const double flash_duration = 1000;
/// <summary>
/// A tracker for periods where alternate should not be forced (i.e. non-gameplay periods).
/// </summary>
/// <remarks>
/// This is different from <see cref="Player.IsBreakTime"/> in that the periods here end strictly at the first object after the break, rather than the break's end time.
/// </remarks>
private PeriodTracker nonGameplayPeriods;
private OsuAction? lastActionPressed;
private DrawableRuleset<OsuHitObject> ruleset;
private IFrameStableClock gameplayClock;
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{
ruleset = drawableRuleset;
drawableRuleset.KeyBindingInputManager.Add(new InputInterceptor(this));
var periods = new List<Period>();
if (drawableRuleset.Objects.Any())
{
periods.Add(new Period(int.MinValue, getValidJudgementTime(ruleset.Objects.First()) - 1));
foreach (BreakPeriod b in drawableRuleset.Beatmap.Breaks)
periods.Add(new Period(b.StartTime, getValidJudgementTime(ruleset.Objects.First(h => h.StartTime >= b.EndTime)) - 1));
static double getValidJudgementTime(HitObject hitObject) => hitObject.StartTime - hitObject.HitWindows.WindowFor(HitResult.Meh);
}
nonGameplayPeriods = new PeriodTracker(periods);
gameplayClock = drawableRuleset.FrameStableClock;
}
private bool checkCorrectAction(OsuAction action)
{
if (nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime))
{
lastActionPressed = null;
return true;
}
switch (action)
{
case OsuAction.LeftButton:
case OsuAction.RightButton:
break;
// Any action which is not left or right button should be ignored.
default:
return true;
}
if (lastActionPressed != action)
{
// User alternated correctly.
lastActionPressed = action;
return true;
}
ruleset.Cursor.FlashColour(Colour4.Red, flash_duration, Easing.OutQuint);
return false;
}
private class InputInterceptor : Component, IKeyBindingHandler<OsuAction>
{
private readonly OsuModAlternate mod;
public InputInterceptor(OsuModAlternate mod)
{
this.mod = mod;
}
public bool OnPressed(KeyBindingPressEvent<OsuAction> e)
// if the pressed action is incorrect, block it from reaching gameplay.
=> !mod.checkCorrectAction(e.Action);
public void OnReleased(KeyBindingReleaseEvent<OsuAction> e)
{
}
}
protected override bool CheckValidNewAction(OsuAction action) => LastAcceptedAction != action;
}
}

View File

@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModAutoplay : ModAutoplay
{
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModRepel), typeof(OsuModAutopilot), typeof(OsuModSpunOut), typeof(OsuModAlternate) }).ToArray();
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModRepel), typeof(OsuModAutopilot), typeof(OsuModSpunOut), typeof(OsuModAlternate), typeof(OsuModSingleTap) }).ToArray();
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" });

View File

@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModCinema : ModCinema<OsuHitObject>
{
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModAutopilot), typeof(OsuModSpunOut), typeof(OsuModAlternate) }).ToArray();
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModAutopilot), typeof(OsuModSpunOut), typeof(OsuModAlternate), typeof(OsuModSingleTap) }).ToArray();
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" });

View File

@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToPlayer
{
public override string Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things.";
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModMagnetised), typeof(OsuModAlternate) }).ToArray();
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModMagnetised), typeof(OsuModAlternate), typeof(OsuModSingleTap) }).ToArray();
/// <summary>
/// How early before a hitobject's start time to trigger a hit.

View File

@ -0,0 +1,18 @@
// 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.Linq;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModSingleTap : InputBlockingMod
{
public override string Name => @"Single Tap";
public override string Acronym => @"ST";
public override string Description => @"You must only use one key!";
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAlternate) }).ToArray();
protected override bool CheckValidNewAction(OsuAction action) => LastAcceptedAction == null || LastAcceptedAction == action;
}
}

View File

@ -172,7 +172,7 @@ namespace osu.Game.Rulesets.Osu
new OsuModClassic(),
new OsuModRandom(),
new OsuModMirror(),
new OsuModAlternate(),
new MultiMod(new OsuModAlternate(), new OsuModSingleTap())
};
case ModType.Automation:

View File

@ -4,9 +4,12 @@
#nullable disable
using System;
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Extensions;
using osu.Game.Models;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.NonVisual
{
@ -23,6 +26,47 @@ namespace osu.Game.Tests.NonVisual
Assert.IsTrue(ourInfo.MatchesOnlineID(otherInfo));
}
[Test]
public void TestAudioEqualityNoFile()
{
var beatmapSetA = TestResources.CreateTestBeatmapSetInfo(1);
var beatmapSetB = TestResources.CreateTestBeatmapSetInfo(1);
Assert.AreNotEqual(beatmapSetA, beatmapSetB);
Assert.IsTrue(beatmapSetA.Beatmaps.Single().AudioEquals(beatmapSetB.Beatmaps.Single()));
}
[Test]
public void TestAudioEqualitySameHash()
{
var beatmapSetA = TestResources.CreateTestBeatmapSetInfo(1);
var beatmapSetB = TestResources.CreateTestBeatmapSetInfo(1);
addAudioFile(beatmapSetA, "abc");
addAudioFile(beatmapSetB, "abc");
Assert.AreNotEqual(beatmapSetA, beatmapSetB);
Assert.IsTrue(beatmapSetA.Beatmaps.Single().AudioEquals(beatmapSetB.Beatmaps.Single()));
}
[Test]
public void TestAudioEqualityDifferentHash()
{
var beatmapSetA = TestResources.CreateTestBeatmapSetInfo(1);
var beatmapSetB = TestResources.CreateTestBeatmapSetInfo(1);
addAudioFile(beatmapSetA);
addAudioFile(beatmapSetB);
Assert.AreNotEqual(beatmapSetA, beatmapSetB);
Assert.IsTrue(beatmapSetA.Beatmaps.Single().AudioEquals(beatmapSetB.Beatmaps.Single()));
}
private static void addAudioFile(BeatmapSetInfo beatmapSetInfo, string hash = null)
{
beatmapSetInfo.Files.Add(new RealmNamedFileUsage(new RealmFile { Hash = hash ?? Guid.NewGuid().ToString() }, "audio.mp3"));
}
[Test]
public void TestDatabasedWithDatabased()
{

View File

@ -134,6 +134,7 @@ namespace osu.Game.Tests.Resources
DifficultyName = $"{version} {beatmapId} (length {TimeSpan.FromMilliseconds(length):m\\:ss}, bpm {bpm:0.#})",
StarRating = diff,
Length = length,
BeatmapSet = beatmapSet,
BPM = bpm,
Hash = Guid.NewGuid().ToString().ComputeMD5Hash(),
Ruleset = rulesetInfo,

View File

@ -6,6 +6,7 @@ using System.IO;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Screens;
@ -103,6 +104,8 @@ namespace osu.Game.Tests.Visual.Editing
*/
public void TestAddAudioTrack()
{
AddAssert("track is virtual", () => Beatmap.Value.Track is TrackVirtual);
AddAssert("switch track to real track", () =>
{
var setup = Editor.ChildrenOfType<SetupScreen>().First();
@ -131,6 +134,7 @@ namespace osu.Game.Tests.Visual.Editing
}
});
AddAssert("track is not virtual", () => Beatmap.Value.Track is not TrackVirtual);
AddAssert("track length changed", () => Beatmap.Value.Track.Length > 60000);
}

View File

@ -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;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
@ -59,6 +60,20 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override bool AllowFail => false;
[Test]
public void TestLastPlayedUpdated()
{
DateTimeOffset? getLastPlayed() => Realm.Run(r => r.Find<BeatmapInfo>(Beatmap.Value.BeatmapInfo.ID)?.LastPlayed);
AddStep("set no custom ruleset", () => customRuleset = null);
AddAssert("last played is null", () => getLastPlayed() == null);
CreateTest();
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
AddUntilStep("wait for last played to update", () => getLastPlayed() != null);
}
[Test]
public void TestScoreStoredLocally()
{

View File

@ -12,7 +12,8 @@ namespace osu.Game.Tournament.Components
{
public class TournamentSpriteTextWithBackground : CompositeDrawable
{
protected readonly TournamentSpriteText Text;
public readonly TournamentSpriteText Text;
protected readonly Box Background;
public TournamentSpriteTextWithBackground(string text = "")

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using Newtonsoft.Json;
using osu.Framework.Bindables;
@ -13,7 +11,7 @@ namespace osu.Game.Tournament.Models
public int ID;
[JsonProperty("BeatmapInfo")]
public TournamentBeatmap Beatmap;
public TournamentBeatmap? Beatmap;
public long Score;

View File

@ -16,6 +16,10 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
{
private readonly TeamScore score;
private readonly TournamentSpriteTextWithBackground teamText;
private readonly Bindable<string> teamName = new Bindable<string>("???");
private bool showScore;
public bool ShowScore
@ -93,7 +97,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
}
}
},
new TournamentSpriteTextWithBackground(team?.FullName.Value ?? "???")
teamText = new TournamentSpriteTextWithBackground
{
Scale = new Vector2(0.5f),
Origin = anchor,
@ -113,6 +117,11 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
updateDisplay();
FinishTransforms(true);
if (Team != null)
teamName.BindTo(Team.FullName);
teamName.BindValueChanged(name => teamText.Text.Text = name.NewValue, true);
}
private void updateDisplay()

View File

@ -42,6 +42,8 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
currentMatch.BindTo(ladder.CurrentMatch);
currentMatch.BindValueChanged(matchChanged);
currentTeam.BindValueChanged(teamChanged);
updateMatch();
}
@ -67,7 +69,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
// team may change to same team, which means score is not in a good state.
// thus we handle this manually.
teamChanged(currentTeam.Value);
currentTeam.TriggerChange();
}
protected override bool OnMouseDown(MouseDownEvent e)
@ -88,11 +90,11 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
return base.OnMouseDown(e);
}
private void teamChanged(TournamentTeam team)
private void teamChanged(ValueChangedEvent<TournamentTeam> team)
{
InternalChildren = new Drawable[]
{
teamDisplay = new TeamDisplay(team, teamColour, currentTeamScore, currentMatch.Value?.PointsToWin ?? 0),
teamDisplay = new TeamDisplay(team.NewValue, teamColour, currentTeamScore, currentMatch.Value?.PointsToWin ?? 0),
};
}
}

View File

@ -53,6 +53,9 @@ namespace osu.Game.Tournament.Screens.Ladder.Components
editorInfo.Selected.ValueChanged += selection =>
{
// ensure any ongoing edits are committed out to the *current* selection before changing to a new one.
GetContainingInputManager().TriggerFocusContention(null);
roundDropdown.Current = selection.NewValue?.Round;
losersCheckbox.Current = selection.NewValue?.Losers;
dateTimeBox.Current = selection.NewValue?.Date;

View File

@ -3,6 +3,7 @@
#nullable disable
using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@ -69,7 +70,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro
currentTeam.BindValueChanged(teamChanged, true);
}
private void teamChanged(ValueChangedEvent<TournamentTeam> team)
private void teamChanged(ValueChangedEvent<TournamentTeam> team) => Scheduler.AddOnce(() =>
{
if (team.NewValue == null)
{
@ -78,7 +79,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro
}
showTeam(team.NewValue);
}
});
protected override void CurrentMatchChanged(ValueChangedEvent<TournamentMatch> match)
{
@ -120,8 +121,14 @@ namespace osu.Game.Tournament.Screens.TeamIntro
foreach (var seeding in team.SeedingResults)
{
fill.Add(new ModRow(seeding.Mod.Value, seeding.Seed.Value));
foreach (var beatmap in seeding.Beatmaps)
{
if (beatmap.Beatmap == null)
continue;
fill.Add(new BeatmapScoreRow(beatmap));
}
}
}
@ -129,6 +136,8 @@ namespace osu.Game.Tournament.Screens.TeamIntro
{
public BeatmapScoreRow(SeedingBeatmap beatmap)
{
Debug.Assert(beatmap.Beatmap != null);
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
@ -157,7 +166,8 @@ namespace osu.Game.Tournament.Screens.TeamIntro
Children = new Drawable[]
{
new TournamentSpriteText { Text = beatmap.Score.ToString("#,0"), Colour = TournamentGame.TEXT_COLOUR, Width = 80 },
new TournamentSpriteText { Text = "#" + beatmap.Seed.Value.ToString("#,0"), Colour = TournamentGame.TEXT_COLOUR, Font = OsuFont.Torus.With(weight: FontWeight.Regular) },
new TournamentSpriteText
{ Text = "#" + beatmap.Seed.Value.ToString("#,0"), Colour = TournamentGame.TEXT_COLOUR, Font = OsuFont.Torus.With(weight: FontWeight.Regular) },
}
},
};

View File

@ -66,7 +66,7 @@ namespace osu.Game.Tournament.Screens.TeamWin
private bool firstDisplay = true;
private void update() => Schedule(() =>
private void update() => Scheduler.AddOnce(() =>
{
var match = CurrentMatch.Value;

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics;
using System.Linq;
using JetBrains.Annotations;
using Newtonsoft.Json;
@ -110,6 +111,11 @@ namespace osu.Game.Beatmaps
public bool SamplesMatchPlaybackRate { get; set; } = true;
/// <summary>
/// The time at which this beatmap was last played by the local user.
/// </summary>
public DateTimeOffset? LastPlayed { get; set; }
/// <summary>
/// The ratio of distance travelled per time unit.
/// Generally used to decouple the spacing between hit objects from the enforced "velocity" of the beatmap (see <see cref="DifficultyControlPoint.SliderVelocity"/>).
@ -151,14 +157,23 @@ namespace osu.Game.Beatmaps
public bool AudioEquals(BeatmapInfo? other) => other != null
&& BeatmapSet != null
&& other.BeatmapSet != null
&& BeatmapSet.Hash == other.BeatmapSet.Hash
&& Metadata.AudioFile == other.Metadata.AudioFile;
&& compareFiles(this, other, m => m.AudioFile);
public bool BackgroundEquals(BeatmapInfo? other) => other != null
&& BeatmapSet != null
&& other.BeatmapSet != null
&& BeatmapSet.Hash == other.BeatmapSet.Hash
&& Metadata.BackgroundFile == other.Metadata.BackgroundFile;
&& compareFiles(this, other, m => m.BackgroundFile);
private static bool compareFiles(BeatmapInfo x, BeatmapInfo y, Func<IBeatmapMetadataInfo, string> getFilename)
{
Debug.Assert(x.BeatmapSet != null);
Debug.Assert(y.BeatmapSet != null);
string? fileHashX = x.BeatmapSet.Files.FirstOrDefault(f => f.Filename == getFilename(x.BeatmapSet.Metadata))?.File.Hash;
string? fileHashY = y.BeatmapSet.Files.FirstOrDefault(f => f.Filename == getFilename(y.BeatmapSet.Metadata))?.File.Hash;
return fileHashX == fileHashY;
}
IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata;
IBeatmapSetInfo? IBeatmapInfo.BeatmapSet => BeatmapSet;

View File

@ -58,8 +58,9 @@ namespace osu.Game.Database
/// 12 2021-11-24 Add Status to RealmBeatmapSet.
/// 13 2022-01-13 Final migration of beatmaps and scores to realm (multiple new storage fields).
/// 14 2022-03-01 Added BeatmapUserSettings to BeatmapInfo.
/// 15 2022-07-13 Added LastPlayed to BeatmapInfo.
/// </summary>
private const int schema_version = 14;
private const int schema_version = 15;
/// <summary>
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.

View File

@ -94,6 +94,8 @@ namespace osu.Game.IO
error = OsuStorageError.None;
Storage lastStorage = UnderlyingStorage;
Logger.Log($"Attempting to use custom storage location {CustomStoragePath}");
try
{
Storage userStorage = host.GetStorage(CustomStoragePath);
@ -102,6 +104,7 @@ namespace osu.Game.IO
error = OsuStorageError.AccessibleButEmpty;
ChangeTargetStorage(userStorage);
Logger.Log($"Storage successfully changed to {CustomStoragePath}.");
}
catch
{
@ -109,6 +112,9 @@ namespace osu.Game.IO
ChangeTargetStorage(lastStorage);
}
if (error != OsuStorageError.None)
Logger.Log($"Custom storage location could not be used ({error}).");
return error == OsuStorageError.None;
}

View File

@ -186,7 +186,7 @@ namespace osu.Game.Screens.Edit
loadableBeatmap = beatmapManager.CreateNew(Ruleset.Value, api.LocalUser.Value);
// required so we can get the track length in EditorClock.
// this is safe as nothing has yet got a reference to this new beatmap.
// this is ONLY safe because the track being provided is a `TrackVirtual` which we don't really care about disposing.
loadableBeatmap.LoadTrack();
// this is a bit haphazard, but guards against setting the lease Beatmap bindable if

View File

@ -35,7 +35,13 @@ namespace osu.Game.Screens.Edit.GameplayTest
ScoreProcessor.HasCompleted.BindValueChanged(completed =>
{
if (completed.NewValue)
Scheduler.AddDelayed(this.Exit, RESULTS_DISPLAY_DELAY);
{
Scheduler.AddDelayed(() =>
{
if (this.IsCurrentScreen())
this.Exit();
}, RESULTS_DISPLAY_DELAY);
}
});
}

View File

@ -11,6 +11,8 @@ using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Scoring;
@ -117,6 +119,23 @@ namespace osu.Game.Screens.Play
await submitScore(score).ConfigureAwait(false);
}
[Resolved]
private RealmAccess realm { get; set; }
protected override void StartGameplay()
{
base.StartGameplay();
// User expectation is that last played should be updated when entering the gameplay loop
// from multiplayer / playlists / solo.
realm.WriteAsync(r =>
{
var realmBeatmap = r.Find<BeatmapInfo>(Beatmap.Value.BeatmapInfo.ID);
if (realmBeatmap != null)
realmBeatmap.LastPlayed = DateTimeOffset.Now;
});
}
public override bool OnExiting(ScreenExitEvent e)
{
bool exiting = base.OnExiting(e);

View File

@ -81,6 +81,9 @@ namespace osu.Game.Screens.Select.Carousel
case SortMode.DateAdded:
return otherSet.BeatmapSet.DateAdded.CompareTo(BeatmapSet.DateAdded);
case SortMode.LastPlayed:
return -compareUsingAggregateMax(otherSet, b => (b.LastPlayed ?? DateTimeOffset.MinValue).ToUnixTimeSeconds());
case SortMode.BPM:
return compareUsingAggregateMax(otherSet, b => b.BPM);

View File

@ -23,6 +23,9 @@ namespace osu.Game.Screens.Select.Filter
[Description("Date Added")]
DateAdded,
[Description("Last Played")]
LastPlayed,
[LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.ListingSearchSortingDifficulty))]
Difficulty,

View File

@ -68,7 +68,7 @@ namespace osu.Game.Screens.Select
Current.BindValueChanged(_ => updateMultiplierText(), true);
}
private void updateMultiplierText()
private void updateMultiplierText() => Schedule(() =>
{
double multiplier = Current.Value?.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier) ?? 1;
@ -85,6 +85,6 @@ namespace osu.Game.Screens.Select
modDisplay.FadeIn();
else
modDisplay.FadeOut();
}
});
}
}

View File

@ -36,7 +36,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Realm" Version="10.14.0" />
<PackageReference Include="ppy.osu.Framework" Version="2022.707.0" />
<PackageReference Include="ppy.osu.Framework" Version="2022.713.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.702.0" />
<PackageReference Include="Sentry" Version="3.19.0" />
<PackageReference Include="SharpCompress" Version="0.32.1" />

View File

@ -61,7 +61,7 @@
<Reference Include="System.Net.Http" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2022.707.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2022.713.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.702.0" />
</ItemGroup>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net6.0) -->
@ -84,7 +84,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="5.0.14" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="ppy.osu.Framework" Version="2022.707.0" />
<PackageReference Include="ppy.osu.Framework" Version="2022.713.0" />
<PackageReference Include="SharpCompress" Version="0.32.1" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />