mirror of
https://github.com/ppy/osu.git
synced 2025-01-26 16:12:54 +08:00
Merge branch 'master' into tourney-save-changes-button-ux
This commit is contained in:
commit
2734a28115
@ -52,7 +52,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.702.0" />
|
<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>
|
||||||
<ItemGroup Label="Transitive Dependencies">
|
<ItemGroup Label="Transitive Dependencies">
|
||||||
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
|
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
|
||||||
|
175
osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSingleTap.cs
Normal file
175
osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSingleTap.cs
Normal 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)),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
114
osu.Game.Rulesets.Osu/Mods/InputBlockingMod.cs
Normal file
114
osu.Game.Rulesets.Osu/Mods/InputBlockingMod.cs
Normal 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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,119 +1,20 @@
|
|||||||
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Graphics;
|
|
||||||
using osu.Framework.Graphics.Sprites;
|
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
|
namespace osu.Game.Rulesets.Osu.Mods
|
||||||
{
|
{
|
||||||
public class OsuModAlternate : Mod, IApplicableToDrawableRuleset<OsuHitObject>
|
public class OsuModAlternate : InputBlockingMod
|
||||||
{
|
{
|
||||||
public override string Name => @"Alternate";
|
public override string Name => @"Alternate";
|
||||||
public override string Acronym => @"AL";
|
public override string Acronym => @"AL";
|
||||||
public override string Description => @"Don't use the same key twice in a row!";
|
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 IconUsage? Icon => FontAwesome.Solid.Keyboard;
|
||||||
|
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModSingleTap) }).ToArray();
|
||||||
|
|
||||||
private const double flash_duration = 1000;
|
protected override bool CheckValidNewAction(OsuAction action) => LastAcceptedAction != action;
|
||||||
|
|
||||||
/// <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)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
{
|
{
|
||||||
public class OsuModAutoplay : ModAutoplay
|
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)
|
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
|
||||||
=> new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" });
|
=> new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" });
|
||||||
|
@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
{
|
{
|
||||||
public class OsuModCinema : ModCinema<OsuHitObject>
|
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)
|
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
|
||||||
=> new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" });
|
=> new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" });
|
||||||
|
@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToPlayer
|
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 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>
|
/// <summary>
|
||||||
/// How early before a hitobject's start time to trigger a hit.
|
/// How early before a hitobject's start time to trigger a hit.
|
||||||
|
18
osu.Game.Rulesets.Osu/Mods/OsuModSingleTap.cs
Normal file
18
osu.Game.Rulesets.Osu/Mods/OsuModSingleTap.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -172,7 +172,7 @@ namespace osu.Game.Rulesets.Osu
|
|||||||
new OsuModClassic(),
|
new OsuModClassic(),
|
||||||
new OsuModRandom(),
|
new OsuModRandom(),
|
||||||
new OsuModMirror(),
|
new OsuModMirror(),
|
||||||
new OsuModAlternate(),
|
new MultiMod(new OsuModAlternate(), new OsuModSingleTap())
|
||||||
};
|
};
|
||||||
|
|
||||||
case ModType.Automation:
|
case ModType.Automation:
|
||||||
|
@ -4,9 +4,12 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Extensions;
|
using osu.Game.Extensions;
|
||||||
|
using osu.Game.Models;
|
||||||
|
using osu.Game.Tests.Resources;
|
||||||
|
|
||||||
namespace osu.Game.Tests.NonVisual
|
namespace osu.Game.Tests.NonVisual
|
||||||
{
|
{
|
||||||
@ -23,6 +26,47 @@ namespace osu.Game.Tests.NonVisual
|
|||||||
Assert.IsTrue(ourInfo.MatchesOnlineID(otherInfo));
|
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]
|
[Test]
|
||||||
public void TestDatabasedWithDatabased()
|
public void TestDatabasedWithDatabased()
|
||||||
{
|
{
|
||||||
|
@ -134,6 +134,7 @@ namespace osu.Game.Tests.Resources
|
|||||||
DifficultyName = $"{version} {beatmapId} (length {TimeSpan.FromMilliseconds(length):m\\:ss}, bpm {bpm:0.#})",
|
DifficultyName = $"{version} {beatmapId} (length {TimeSpan.FromMilliseconds(length):m\\:ss}, bpm {bpm:0.#})",
|
||||||
StarRating = diff,
|
StarRating = diff,
|
||||||
Length = length,
|
Length = length,
|
||||||
|
BeatmapSet = beatmapSet,
|
||||||
BPM = bpm,
|
BPM = bpm,
|
||||||
Hash = Guid.NewGuid().ToString().ComputeMD5Hash(),
|
Hash = Guid.NewGuid().ToString().ComputeMD5Hash(),
|
||||||
Ruleset = rulesetInfo,
|
Ruleset = rulesetInfo,
|
||||||
|
@ -6,6 +6,7 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Audio.Track;
|
||||||
using osu.Framework.Extensions.ObjectExtensions;
|
using osu.Framework.Extensions.ObjectExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Screens;
|
using osu.Framework.Screens;
|
||||||
@ -103,6 +104,8 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
*/
|
*/
|
||||||
public void TestAddAudioTrack()
|
public void TestAddAudioTrack()
|
||||||
{
|
{
|
||||||
|
AddAssert("track is virtual", () => Beatmap.Value.Track is TrackVirtual);
|
||||||
|
|
||||||
AddAssert("switch track to real track", () =>
|
AddAssert("switch track to real track", () =>
|
||||||
{
|
{
|
||||||
var setup = Editor.ChildrenOfType<SetupScreen>().First();
|
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);
|
AddAssert("track length changed", () => Beatmap.Value.Track.Length > 60000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
@ -59,6 +60,20 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
|
|
||||||
protected override bool AllowFail => false;
|
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]
|
[Test]
|
||||||
public void TestScoreStoredLocally()
|
public void TestScoreStoredLocally()
|
||||||
{
|
{
|
||||||
|
@ -12,7 +12,8 @@ namespace osu.Game.Tournament.Components
|
|||||||
{
|
{
|
||||||
public class TournamentSpriteTextWithBackground : CompositeDrawable
|
public class TournamentSpriteTextWithBackground : CompositeDrawable
|
||||||
{
|
{
|
||||||
protected readonly TournamentSpriteText Text;
|
public readonly TournamentSpriteText Text;
|
||||||
|
|
||||||
protected readonly Box Background;
|
protected readonly Box Background;
|
||||||
|
|
||||||
public TournamentSpriteTextWithBackground(string text = "")
|
public TournamentSpriteTextWithBackground(string text = "")
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
|
|
||||||
@ -13,7 +11,7 @@ namespace osu.Game.Tournament.Models
|
|||||||
public int ID;
|
public int ID;
|
||||||
|
|
||||||
[JsonProperty("BeatmapInfo")]
|
[JsonProperty("BeatmapInfo")]
|
||||||
public TournamentBeatmap Beatmap;
|
public TournamentBeatmap? Beatmap;
|
||||||
|
|
||||||
public long Score;
|
public long Score;
|
||||||
|
|
||||||
|
@ -16,6 +16,10 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
|
|||||||
{
|
{
|
||||||
private readonly TeamScore score;
|
private readonly TeamScore score;
|
||||||
|
|
||||||
|
private readonly TournamentSpriteTextWithBackground teamText;
|
||||||
|
|
||||||
|
private readonly Bindable<string> teamName = new Bindable<string>("???");
|
||||||
|
|
||||||
private bool showScore;
|
private bool showScore;
|
||||||
|
|
||||||
public 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),
|
Scale = new Vector2(0.5f),
|
||||||
Origin = anchor,
|
Origin = anchor,
|
||||||
@ -113,6 +117,11 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
|
|||||||
|
|
||||||
updateDisplay();
|
updateDisplay();
|
||||||
FinishTransforms(true);
|
FinishTransforms(true);
|
||||||
|
|
||||||
|
if (Team != null)
|
||||||
|
teamName.BindTo(Team.FullName);
|
||||||
|
|
||||||
|
teamName.BindValueChanged(name => teamText.Text.Text = name.NewValue, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateDisplay()
|
private void updateDisplay()
|
||||||
|
@ -42,6 +42,8 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
|
|||||||
currentMatch.BindTo(ladder.CurrentMatch);
|
currentMatch.BindTo(ladder.CurrentMatch);
|
||||||
currentMatch.BindValueChanged(matchChanged);
|
currentMatch.BindValueChanged(matchChanged);
|
||||||
|
|
||||||
|
currentTeam.BindValueChanged(teamChanged);
|
||||||
|
|
||||||
updateMatch();
|
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.
|
// team may change to same team, which means score is not in a good state.
|
||||||
// thus we handle this manually.
|
// thus we handle this manually.
|
||||||
teamChanged(currentTeam.Value);
|
currentTeam.TriggerChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool OnMouseDown(MouseDownEvent e)
|
protected override bool OnMouseDown(MouseDownEvent e)
|
||||||
@ -88,11 +90,11 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
|
|||||||
return base.OnMouseDown(e);
|
return base.OnMouseDown(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void teamChanged(TournamentTeam team)
|
private void teamChanged(ValueChangedEvent<TournamentTeam> team)
|
||||||
{
|
{
|
||||||
InternalChildren = new Drawable[]
|
InternalChildren = new Drawable[]
|
||||||
{
|
{
|
||||||
teamDisplay = new TeamDisplay(team, teamColour, currentTeamScore, currentMatch.Value?.PointsToWin ?? 0),
|
teamDisplay = new TeamDisplay(team.NewValue, teamColour, currentTeamScore, currentMatch.Value?.PointsToWin ?? 0),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,6 +53,9 @@ namespace osu.Game.Tournament.Screens.Ladder.Components
|
|||||||
|
|
||||||
editorInfo.Selected.ValueChanged += selection =>
|
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;
|
roundDropdown.Current = selection.NewValue?.Round;
|
||||||
losersCheckbox.Current = selection.NewValue?.Losers;
|
losersCheckbox.Current = selection.NewValue?.Losers;
|
||||||
dateTimeBox.Current = selection.NewValue?.Date;
|
dateTimeBox.Current = selection.NewValue?.Date;
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
|
using System.Diagnostics;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
@ -69,7 +70,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro
|
|||||||
currentTeam.BindValueChanged(teamChanged, true);
|
currentTeam.BindValueChanged(teamChanged, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void teamChanged(ValueChangedEvent<TournamentTeam> team)
|
private void teamChanged(ValueChangedEvent<TournamentTeam> team) => Scheduler.AddOnce(() =>
|
||||||
{
|
{
|
||||||
if (team.NewValue == null)
|
if (team.NewValue == null)
|
||||||
{
|
{
|
||||||
@ -78,7 +79,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro
|
|||||||
}
|
}
|
||||||
|
|
||||||
showTeam(team.NewValue);
|
showTeam(team.NewValue);
|
||||||
}
|
});
|
||||||
|
|
||||||
protected override void CurrentMatchChanged(ValueChangedEvent<TournamentMatch> match)
|
protected override void CurrentMatchChanged(ValueChangedEvent<TournamentMatch> match)
|
||||||
{
|
{
|
||||||
@ -120,8 +121,14 @@ namespace osu.Game.Tournament.Screens.TeamIntro
|
|||||||
foreach (var seeding in team.SeedingResults)
|
foreach (var seeding in team.SeedingResults)
|
||||||
{
|
{
|
||||||
fill.Add(new ModRow(seeding.Mod.Value, seeding.Seed.Value));
|
fill.Add(new ModRow(seeding.Mod.Value, seeding.Seed.Value));
|
||||||
|
|
||||||
foreach (var beatmap in seeding.Beatmaps)
|
foreach (var beatmap in seeding.Beatmaps)
|
||||||
|
{
|
||||||
|
if (beatmap.Beatmap == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
fill.Add(new BeatmapScoreRow(beatmap));
|
fill.Add(new BeatmapScoreRow(beatmap));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,6 +136,8 @@ namespace osu.Game.Tournament.Screens.TeamIntro
|
|||||||
{
|
{
|
||||||
public BeatmapScoreRow(SeedingBeatmap beatmap)
|
public BeatmapScoreRow(SeedingBeatmap beatmap)
|
||||||
{
|
{
|
||||||
|
Debug.Assert(beatmap.Beatmap != null);
|
||||||
|
|
||||||
RelativeSizeAxes = Axes.X;
|
RelativeSizeAxes = Axes.X;
|
||||||
AutoSizeAxes = Axes.Y;
|
AutoSizeAxes = Axes.Y;
|
||||||
|
|
||||||
@ -157,7 +166,8 @@ namespace osu.Game.Tournament.Screens.TeamIntro
|
|||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
new TournamentSpriteText { Text = beatmap.Score.ToString("#,0"), Colour = TournamentGame.TEXT_COLOUR, Width = 80 },
|
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) },
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -66,7 +66,7 @@ namespace osu.Game.Tournament.Screens.TeamWin
|
|||||||
|
|
||||||
private bool firstDisplay = true;
|
private bool firstDisplay = true;
|
||||||
|
|
||||||
private void update() => Schedule(() =>
|
private void update() => Scheduler.AddOnce(() =>
|
||||||
{
|
{
|
||||||
var match = CurrentMatch.Value;
|
var match = CurrentMatch.Value;
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
@ -110,6 +111,11 @@ namespace osu.Game.Beatmaps
|
|||||||
|
|
||||||
public bool SamplesMatchPlaybackRate { get; set; } = true;
|
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>
|
/// <summary>
|
||||||
/// The ratio of distance travelled per time unit.
|
/// 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"/>).
|
/// 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
|
public bool AudioEquals(BeatmapInfo? other) => other != null
|
||||||
&& BeatmapSet != null
|
&& BeatmapSet != null
|
||||||
&& other.BeatmapSet != null
|
&& other.BeatmapSet != null
|
||||||
&& BeatmapSet.Hash == other.BeatmapSet.Hash
|
&& compareFiles(this, other, m => m.AudioFile);
|
||||||
&& Metadata.AudioFile == other.Metadata.AudioFile;
|
|
||||||
|
|
||||||
public bool BackgroundEquals(BeatmapInfo? other) => other != null
|
public bool BackgroundEquals(BeatmapInfo? other) => other != null
|
||||||
&& BeatmapSet != null
|
&& BeatmapSet != null
|
||||||
&& other.BeatmapSet != null
|
&& other.BeatmapSet != null
|
||||||
&& BeatmapSet.Hash == other.BeatmapSet.Hash
|
&& compareFiles(this, other, m => m.BackgroundFile);
|
||||||
&& Metadata.BackgroundFile == other.Metadata.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;
|
IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata;
|
||||||
IBeatmapSetInfo? IBeatmapInfo.BeatmapSet => BeatmapSet;
|
IBeatmapSetInfo? IBeatmapInfo.BeatmapSet => BeatmapSet;
|
||||||
|
@ -58,8 +58,9 @@ namespace osu.Game.Database
|
|||||||
/// 12 2021-11-24 Add Status to RealmBeatmapSet.
|
/// 12 2021-11-24 Add Status to RealmBeatmapSet.
|
||||||
/// 13 2022-01-13 Final migration of beatmaps and scores to realm (multiple new storage fields).
|
/// 13 2022-01-13 Final migration of beatmaps and scores to realm (multiple new storage fields).
|
||||||
/// 14 2022-03-01 Added BeatmapUserSettings to BeatmapInfo.
|
/// 14 2022-03-01 Added BeatmapUserSettings to BeatmapInfo.
|
||||||
|
/// 15 2022-07-13 Added LastPlayed to BeatmapInfo.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private const int schema_version = 14;
|
private const int schema_version = 15;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
|
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
|
||||||
|
@ -94,6 +94,8 @@ namespace osu.Game.IO
|
|||||||
error = OsuStorageError.None;
|
error = OsuStorageError.None;
|
||||||
Storage lastStorage = UnderlyingStorage;
|
Storage lastStorage = UnderlyingStorage;
|
||||||
|
|
||||||
|
Logger.Log($"Attempting to use custom storage location {CustomStoragePath}");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Storage userStorage = host.GetStorage(CustomStoragePath);
|
Storage userStorage = host.GetStorage(CustomStoragePath);
|
||||||
@ -102,6 +104,7 @@ namespace osu.Game.IO
|
|||||||
error = OsuStorageError.AccessibleButEmpty;
|
error = OsuStorageError.AccessibleButEmpty;
|
||||||
|
|
||||||
ChangeTargetStorage(userStorage);
|
ChangeTargetStorage(userStorage);
|
||||||
|
Logger.Log($"Storage successfully changed to {CustomStoragePath}.");
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@ -109,6 +112,9 @@ namespace osu.Game.IO
|
|||||||
ChangeTargetStorage(lastStorage);
|
ChangeTargetStorage(lastStorage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (error != OsuStorageError.None)
|
||||||
|
Logger.Log($"Custom storage location could not be used ({error}).");
|
||||||
|
|
||||||
return error == OsuStorageError.None;
|
return error == OsuStorageError.None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,7 +186,7 @@ namespace osu.Game.Screens.Edit
|
|||||||
loadableBeatmap = beatmapManager.CreateNew(Ruleset.Value, api.LocalUser.Value);
|
loadableBeatmap = beatmapManager.CreateNew(Ruleset.Value, api.LocalUser.Value);
|
||||||
|
|
||||||
// required so we can get the track length in EditorClock.
|
// 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();
|
loadableBeatmap.LoadTrack();
|
||||||
|
|
||||||
// this is a bit haphazard, but guards against setting the lease Beatmap bindable if
|
// this is a bit haphazard, but guards against setting the lease Beatmap bindable if
|
||||||
|
@ -35,7 +35,13 @@ namespace osu.Game.Screens.Edit.GameplayTest
|
|||||||
ScoreProcessor.HasCompleted.BindValueChanged(completed =>
|
ScoreProcessor.HasCompleted.BindValueChanged(completed =>
|
||||||
{
|
{
|
||||||
if (completed.NewValue)
|
if (completed.NewValue)
|
||||||
Scheduler.AddDelayed(this.Exit, RESULTS_DISPLAY_DELAY);
|
{
|
||||||
|
Scheduler.AddDelayed(() =>
|
||||||
|
{
|
||||||
|
if (this.IsCurrentScreen())
|
||||||
|
this.Exit();
|
||||||
|
}, RESULTS_DISPLAY_DELAY);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,6 +11,8 @@ using osu.Framework.Allocation;
|
|||||||
using osu.Framework.Extensions;
|
using osu.Framework.Extensions;
|
||||||
using osu.Framework.Logging;
|
using osu.Framework.Logging;
|
||||||
using osu.Framework.Screens;
|
using osu.Framework.Screens;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Database;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Online.Rooms;
|
using osu.Game.Online.Rooms;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
@ -117,6 +119,23 @@ namespace osu.Game.Screens.Play
|
|||||||
await submitScore(score).ConfigureAwait(false);
|
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)
|
public override bool OnExiting(ScreenExitEvent e)
|
||||||
{
|
{
|
||||||
bool exiting = base.OnExiting(e);
|
bool exiting = base.OnExiting(e);
|
||||||
|
@ -81,6 +81,9 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
case SortMode.DateAdded:
|
case SortMode.DateAdded:
|
||||||
return otherSet.BeatmapSet.DateAdded.CompareTo(BeatmapSet.DateAdded);
|
return otherSet.BeatmapSet.DateAdded.CompareTo(BeatmapSet.DateAdded);
|
||||||
|
|
||||||
|
case SortMode.LastPlayed:
|
||||||
|
return -compareUsingAggregateMax(otherSet, b => (b.LastPlayed ?? DateTimeOffset.MinValue).ToUnixTimeSeconds());
|
||||||
|
|
||||||
case SortMode.BPM:
|
case SortMode.BPM:
|
||||||
return compareUsingAggregateMax(otherSet, b => b.BPM);
|
return compareUsingAggregateMax(otherSet, b => b.BPM);
|
||||||
|
|
||||||
|
@ -23,6 +23,9 @@ namespace osu.Game.Screens.Select.Filter
|
|||||||
[Description("Date Added")]
|
[Description("Date Added")]
|
||||||
DateAdded,
|
DateAdded,
|
||||||
|
|
||||||
|
[Description("Last Played")]
|
||||||
|
LastPlayed,
|
||||||
|
|
||||||
[LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.ListingSearchSortingDifficulty))]
|
[LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.ListingSearchSortingDifficulty))]
|
||||||
Difficulty,
|
Difficulty,
|
||||||
|
|
||||||
|
@ -68,7 +68,7 @@ namespace osu.Game.Screens.Select
|
|||||||
Current.BindValueChanged(_ => updateMultiplierText(), true);
|
Current.BindValueChanged(_ => updateMultiplierText(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateMultiplierText()
|
private void updateMultiplierText() => Schedule(() =>
|
||||||
{
|
{
|
||||||
double multiplier = Current.Value?.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier) ?? 1;
|
double multiplier = Current.Value?.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier) ?? 1;
|
||||||
|
|
||||||
@ -85,6 +85,6 @@ namespace osu.Game.Screens.Select
|
|||||||
modDisplay.FadeIn();
|
modDisplay.FadeIn();
|
||||||
else
|
else
|
||||||
modDisplay.FadeOut();
|
modDisplay.FadeOut();
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,7 @@
|
|||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Realm" Version="10.14.0" />
|
<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="ppy.osu.Game.Resources" Version="2022.702.0" />
|
||||||
<PackageReference Include="Sentry" Version="3.19.0" />
|
<PackageReference Include="Sentry" Version="3.19.0" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.32.1" />
|
<PackageReference Include="SharpCompress" Version="0.32.1" />
|
||||||
|
@ -61,7 +61,7 @@
|
|||||||
<Reference Include="System.Net.Http" />
|
<Reference Include="System.Net.Http" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Package References">
|
<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" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.702.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net6.0) -->
|
<!-- 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" Version="5.0.14" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="5.0.14" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="5.0.14" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
<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="SharpCompress" Version="0.32.1" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||||
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
||||||
|
Loading…
Reference in New Issue
Block a user