1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-26 20:23:00 +08:00

Merge branch 'master' into spectator-state-rework

This commit is contained in:
Dan Balasescu 2022-02-02 18:57:04 +09:00
commit 6d3bc005ea
57 changed files with 752 additions and 93 deletions

View File

@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.EmptyFreeform.Replays
protected override bool IsImportant(EmptyFreeformReplayFrame frame) => frame.Actions.Any();
public override void CollectPendingInputs(List<IInput> inputs)
protected override void CollectReplayInputs(List<IInput> inputs)
{
var position = Interpolation.ValueAt(CurrentTime, StartFrame.Position, EndFrame.Position, StartFrame.Time, EndFrame.Time);

View File

@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Pippidon.Replays
protected override bool IsImportant(PippidonReplayFrame frame) => true;
public override void CollectPendingInputs(List<IInput> inputs)
protected override void CollectReplayInputs(List<IInput> inputs)
{
var position = Interpolation.ValueAt(CurrentTime, StartFrame.Position, EndFrame.Position, StartFrame.Time, EndFrame.Time);

View File

@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.EmptyScrolling.Replays
protected override bool IsImportant(EmptyScrollingReplayFrame frame) => frame.Actions.Any();
public override void CollectPendingInputs(List<IInput> inputs)
protected override void CollectReplayInputs(List<IInput> inputs)
{
inputs.Add(new ReplayState<EmptyScrollingAction>
{

View File

@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Pippidon.Replays
protected override bool IsImportant(PippidonReplayFrame frame) => frame.Actions.Any();
public override void CollectPendingInputs(List<IInput> inputs)
protected override void CollectReplayInputs(List<IInput> inputs)
{
inputs.Add(new ReplayState<PippidonAction>
{

View File

@ -51,7 +51,7 @@
<Reference Include="Java.Interop" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.115.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.202.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.128.0" />
</ItemGroup>
<ItemGroup Label="Transitive Dependencies">

View File

@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Replays
protected override bool IsImportant(CatchReplayFrame frame) => frame.Actions.Any();
public override void CollectPendingInputs(List<IInput> inputs)
protected override void CollectReplayInputs(List<IInput> inputs)
{
float position = Interpolation.ValueAt(CurrentTime, StartFrame.Position, EndFrame.Position, StartFrame.Time, EndFrame.Time);

View File

@ -81,7 +81,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
case CatchSkinComponents.CatchComboCounter:
if (providesComboCounter)
return new LegacyCatchComboCounter(Skin);
return new LegacyCatchComboCounter();
return null;

View File

@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
private readonly LegacyRollingCounter explosion;
public LegacyCatchComboCounter(ISkin skin)
public LegacyCatchComboCounter()
{
AutoSizeAxes = Axes.Both;

View File

@ -0,0 +1,103 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Tests.Visual;
using System.Collections.Generic;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Mania.Beatmaps;
namespace osu.Game.Rulesets.Mania.Tests.Mods
{
public class TestSceneManiaModHoldOff : ModTestScene
{
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
[Test]
public void TestMapHasNoHoldNotes()
{
var testBeatmap = createModdedBeatmap();
Assert.False(testBeatmap.HitObjects.OfType<HoldNote>().Any());
}
[Test]
public void TestCorrectNoteValues()
{
var testBeatmap = createRawBeatmap();
var noteValues = new List<double>(testBeatmap.HitObjects.OfType<HoldNote>().Count());
foreach (HoldNote h in testBeatmap.HitObjects.OfType<HoldNote>())
{
noteValues.Add(ManiaModHoldOff.GetNoteDurationInBeatLength(h, testBeatmap));
}
noteValues.Sort();
Assert.AreEqual(noteValues, new List<double> { 0.125, 0.250, 0.500, 1.000, 2.000 });
}
[Test]
public void TestCorrectObjectCount()
{
// Ensure that the mod produces the expected number of objects when applied.
var rawBeatmap = createRawBeatmap();
var testBeatmap = createModdedBeatmap();
// Calculate expected number of objects
int expectedObjectCount = 0;
foreach (ManiaHitObject h in rawBeatmap.HitObjects)
{
// Both notes and hold notes account for at least one object
expectedObjectCount++;
if (h.GetType() == typeof(HoldNote))
{
double noteValue = ManiaModHoldOff.GetNoteDurationInBeatLength((HoldNote)h, rawBeatmap);
if (noteValue >= ManiaModHoldOff.END_NOTE_ALLOW_THRESHOLD)
{
// Should generate an end note if it's longer than the minimum note value
expectedObjectCount++;
}
}
}
Assert.That(testBeatmap.HitObjects.Count == expectedObjectCount);
}
private static ManiaBeatmap createModdedBeatmap()
{
var beatmap = createRawBeatmap();
var holdOffMod = new ManiaModHoldOff();
foreach (var hitObject in beatmap.HitObjects)
hitObject.ApplyDefaults(beatmap.ControlPointInfo, new BeatmapDifficulty());
holdOffMod.ApplyToBeatmap(beatmap);
return beatmap;
}
private static ManiaBeatmap createRawBeatmap()
{
var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 1 });
beatmap.ControlPointInfo.Add(0.0, new TimingControlPoint { BeatLength = 1000 }); // Set BPM to 60
// Add test hit objects
beatmap.HitObjects.Add(new Note { StartTime = 4000 });
beatmap.HitObjects.Add(new Note { StartTime = 4500 });
beatmap.HitObjects.Add(new HoldNote { StartTime = 0, EndTime = 125 }); // 1/8 note
beatmap.HitObjects.Add(new HoldNote { StartTime = 0, EndTime = 250 }); // 1/4 note
beatmap.HitObjects.Add(new HoldNote { StartTime = 0, EndTime = 500 }); // 1/2 note
beatmap.HitObjects.Add(new HoldNote { StartTime = 0, EndTime = 1000 }); // 1/1 note
beatmap.HitObjects.Add(new HoldNote { StartTime = 0, EndTime = 2000 }); // 2/1 note
return beatmap;
}
}
}

View File

@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
RelativeSizeAxes = Axes.Both,
Width = 0.5f,
Child = new ColumnHitObjectArea(0, new HitObjectContainer())
Child = new ColumnHitObjectArea(new HitObjectContainer())
{
RelativeSizeAxes = Axes.Both
}
@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
RelativeSizeAxes = Axes.Both,
Width = 0.5f,
Child = new ColumnHitObjectArea(1, new HitObjectContainer())
Child = new ColumnHitObjectArea(new HitObjectContainer())
{
RelativeSizeAxes = Axes.Both
}

View File

@ -243,7 +243,8 @@ namespace osu.Game.Rulesets.Mania
new ManiaModDifficultyAdjust(),
new ManiaModClassic(),
new ManiaModInvert(),
new ManiaModConstantSpeed()
new ManiaModConstantSpeed(),
new ManiaModHoldOff()
};
case ModType.Automation:

View File

@ -0,0 +1,72 @@
// 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 osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mods;
using osu.Framework.Graphics.Sprites;
using System.Collections.Generic;
using osu.Game.Rulesets.Mania.Beatmaps;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModHoldOff : Mod, IApplicableAfterBeatmapConversion
{
public override string Name => "Hold Off";
public override string Acronym => "HO";
public override double ScoreMultiplier => 1;
public override string Description => @"Replaces all hold notes with normal notes.";
public override IconUsage? Icon => FontAwesome.Solid.DotCircle;
public override ModType Type => ModType.Conversion;
public override Type[] IncompatibleMods => new[] { typeof(ManiaModInvert) };
public const double END_NOTE_ALLOW_THRESHOLD = 0.5;
public void ApplyToBeatmap(IBeatmap beatmap)
{
var maniaBeatmap = (ManiaBeatmap)beatmap;
var newObjects = new List<ManiaHitObject>();
foreach (var h in beatmap.HitObjects.OfType<HoldNote>())
{
// Add a note for the beginning of the hold note
newObjects.Add(new Note
{
Column = h.Column,
StartTime = h.StartTime,
Samples = h.GetNodeSamples(0)
});
// Don't add an end note if the duration is shorter than the threshold
double noteValue = GetNoteDurationInBeatLength(h, maniaBeatmap); // 1/1, 1/2, 1/4, etc.
if (noteValue >= END_NOTE_ALLOW_THRESHOLD)
{
newObjects.Add(new Note
{
Column = h.Column,
StartTime = h.EndTime,
Samples = h.GetNodeSamples((h.NodeSamples?.Count - 1) ?? 1)
});
}
}
maniaBeatmap.HitObjects = maniaBeatmap.HitObjects.OfType<Note>().Concat(newObjects).OrderBy(h => h.StartTime).ToList();
}
public static double GetNoteDurationInBeatLength(HoldNote holdNote, ManiaBeatmap beatmap)
{
double beatLength = beatmap.ControlPointInfo.TimingPointAt(holdNote.StartTime).BeatLength;
return holdNote.Duration / beatLength;
}
}
}

View File

@ -26,6 +26,8 @@ namespace osu.Game.Rulesets.Mania.Mods
public override ModType Type => ModType.Conversion;
public override Type[] IncompatibleMods => new[] { typeof(ManiaModHoldOff) };
public void ApplyToBeatmap(IBeatmap beatmap)
{
var maniaBeatmap = (ManiaBeatmap)beatmap;

View File

@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Mania.Replays
protected override bool IsImportant(ManiaReplayFrame frame) => frame.Actions.Any();
public override void CollectPendingInputs(List<IInput> inputs)
protected override void CollectReplayInputs(List<IInput> inputs)
{
inputs.Add(new ReplayState<ManiaAction> { PressedActions = CurrentFrame?.Actions ?? new List<ManiaAction>() });
}

View File

@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Mania.UI
sampleTriggerSource = new GameplaySampleTriggerSource(HitObjectContainer),
// For input purposes, the background is added at the highest depth, but is then proxied back below all other elements
background.CreateProxy(),
HitObjectArea = new ColumnHitObjectArea(Index, HitObjectContainer) { RelativeSizeAxes = Axes.Both },
HitObjectArea = new ColumnHitObjectArea(HitObjectContainer) { RelativeSizeAxes = Axes.Both },
new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea), _ => new DefaultKeyArea())
{
RelativeSizeAxes = Axes.Both

View File

@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Mania.UI.Components
private readonly Drawable hitTarget;
public ColumnHitObjectArea(int columnIndex, HitObjectContainer hitObjectContainer)
public ColumnHitObjectArea(HitObjectContainer hitObjectContainer)
: base(hitObjectContainer)
{
AddRangeInternal(new[]

View File

@ -118,7 +118,6 @@ namespace osu.Game.Rulesets.Osu.Tests
public Drawable GetDrawableComponent(ISkinComponent component) => null;
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => null;
public ISample GetSample(ISampleInfo sampleInfo) => null;
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => null;
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
{

View File

@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Replays
protected override bool IsImportant(OsuReplayFrame frame) => frame.Actions.Any();
public override void CollectPendingInputs(List<IInput> inputs)
protected override void CollectReplayInputs(List<IInput> inputs)
{
var position = Interpolation.ValueAt(CurrentTime, StartFrame.Position, EndFrame.Position, StartFrame.Time, EndFrame.Time);

View File

@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Taiko.Replays
protected override bool IsImportant(TaikoReplayFrame frame) => frame.Actions.Any();
public override void CollectPendingInputs(List<IInput> inputs)
protected override void CollectReplayInputs(List<IInput> inputs)
{
inputs.Add(new ReplayState<TaikoAction> { PressedActions = CurrentFrame?.Actions ?? new List<TaikoAction>() });
}

View File

@ -52,7 +52,7 @@ namespace osu.Game.Tests.Editing.Checks
beatmap.BeatmapInfo.BeatmapSet.Files.Add(CheckTestHelpers.CreateMockFile("jpg"));
// Should fail to load, but not produce an error due to the extension not being expected to load.
Assert.IsEmpty(check.Run(getContext(null, allowMissing: true)));
Assert.IsEmpty(check.Run(getContext(null)));
}
[Test]
@ -91,7 +91,7 @@ namespace osu.Game.Tests.Editing.Checks
{
using (var resourceStream = TestResources.OpenResource("Samples/missing.mp3"))
{
Assert.IsEmpty(check.Run(getContext(resourceStream, allowMissing: true)));
Assert.IsEmpty(check.Run(getContext(resourceStream)));
}
}
@ -107,7 +107,7 @@ namespace osu.Game.Tests.Editing.Checks
}
}
private BeatmapVerifierContext getContext(Stream resourceStream, bool allowMissing = false)
private BeatmapVerifierContext getContext(Stream resourceStream)
{
var mockWorkingBeatmap = new Mock<TestWorkingBeatmap>(beatmap, null, null);
mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny<string>())).Returns(resourceStream);

View File

@ -131,8 +131,6 @@ namespace osu.Game.Tests.Gameplay
public ISample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => null;
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
{
switch (lookup)

View File

@ -1,11 +1,17 @@
// 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 NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Online.Spectator;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Tests.Visual;
@ -42,6 +48,43 @@ namespace osu.Game.Tests.Gameplay
Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(Judgement.LARGE_BONUS_SCORE));
}
[Test]
public void TestResetFromReplayFrame()
{
var beatmap = new Beatmap<HitObject> { HitObjects = { new HitCircle() } };
var scoreProcessor = new ScoreProcessor();
scoreProcessor.ApplyBeatmap(beatmap);
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new TestJudgement(HitResult.Great)) { Type = HitResult.Great });
Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(1_000_000));
Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1));
// No header shouldn't cause any change
scoreProcessor.ResetFromReplayFrame(new OsuRuleset(), new OsuReplayFrame());
Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(1_000_000));
Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1));
// Reset with a miss instead.
scoreProcessor.ResetFromReplayFrame(new OsuRuleset(), new OsuReplayFrame
{
Header = new FrameHeader(0, 0, 0, new Dictionary<HitResult, int> { { HitResult.Miss, 1 } }, DateTimeOffset.Now)
});
Assert.That(scoreProcessor.TotalScore.Value, Is.Zero);
Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1));
// Reset with no judged hit.
scoreProcessor.ResetFromReplayFrame(new OsuRuleset(), new OsuReplayFrame
{
Header = new FrameHeader(0, 0, 0, new Dictionary<HitResult, int>(), DateTimeOffset.Now)
});
Assert.That(scoreProcessor.TotalScore.Value, Is.Zero);
Assert.That(scoreProcessor.JudgedHits, Is.Zero);
}
private class TestJudgement : Judgement
{
public override HitResult MaxResult { get; }

View File

@ -61,7 +61,6 @@ namespace osu.Game.Tests.NonVisual.Skinning
public Drawable GetDrawableComponent(ISkinComponent component) => throw new NotSupportedException();
public ISample GetSample(ISampleInfo sampleInfo) => throw new NotSupportedException();
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => throw new NotSupportedException();
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => null;
}
private class TestAnimationTimeReference : IAnimationTimeReference

View File

@ -47,7 +47,7 @@ namespace osu.Game.Tests.Online
{
Dependencies.Cache(rulesets = new RulesetStore(Realm));
Dependencies.CacheAs<BeatmapManager>(beatmaps = new TestBeatmapManager(LocalStorage, Realm, rulesets, API, audio, Resources, host, Beatmap.Default));
Dependencies.CacheAs<BeatmapModelDownloader>(beatmapDownloader = new TestBeatmapModelDownloader(beatmaps, API, host));
Dependencies.CacheAs<BeatmapModelDownloader>(beatmapDownloader = new TestBeatmapModelDownloader(beatmaps, API));
}
[SetUp]
@ -173,14 +173,14 @@ namespace osu.Game.Tests.Online
protected override BeatmapModelManager CreateBeatmapModelManager(Storage storage, RealmAccess realm, RulesetStore rulesets, BeatmapOnlineLookupQueue onlineLookupQueue)
{
return new TestBeatmapModelManager(this, storage, realm, rulesets, onlineLookupQueue);
return new TestBeatmapModelManager(this, storage, realm, onlineLookupQueue);
}
internal class TestBeatmapModelManager : BeatmapModelManager
{
private readonly TestBeatmapManager testBeatmapManager;
public TestBeatmapModelManager(TestBeatmapManager testBeatmapManager, Storage storage, RealmAccess databaseAccess, RulesetStore rulesetStore, BeatmapOnlineLookupQueue beatmapOnlineLookupQueue)
public TestBeatmapModelManager(TestBeatmapManager testBeatmapManager, Storage storage, RealmAccess databaseAccess, BeatmapOnlineLookupQueue beatmapOnlineLookupQueue)
: base(databaseAccess, storage, beatmapOnlineLookupQueue)
{
this.testBeatmapManager = testBeatmapManager;
@ -196,7 +196,7 @@ namespace osu.Game.Tests.Online
internal class TestBeatmapModelDownloader : BeatmapModelDownloader
{
public TestBeatmapModelDownloader(IModelImporter<BeatmapSetInfo> importer, IAPIProvider apiProvider, GameHost gameHost)
public TestBeatmapModelDownloader(IModelImporter<BeatmapSetInfo> importer, IAPIProvider apiProvider)
: base(importer, apiProvider)
{
}

View File

@ -210,7 +210,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
}
public override void CollectPendingInputs(List<IInput> inputs)
protected override void CollectReplayInputs(List<IInput> inputs)
{
inputs.Add(new MousePositionAbsoluteInput { Position = GamefieldToScreenSpace(CurrentFrame?.Position ?? Vector2.Zero) });
inputs.Add(new ReplayState<TestAction> { PressedActions = CurrentFrame?.Actions ?? new List<TestAction>() });

View File

@ -301,8 +301,6 @@ namespace osu.Game.Tests.Visual.Gameplay
public ISample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => throw new NotImplementedException();
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => throw new NotImplementedException();
}
private class SecondarySource : ISkin
@ -314,8 +312,6 @@ namespace osu.Game.Tests.Visual.Gameplay
public ISample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => throw new NotImplementedException();
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => throw new NotImplementedException();
}
[Cached(typeof(ISkinSource))]

View File

@ -218,6 +218,22 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("last received frame has time = 1000", () => spectatorClient.LastReceivedUserFrames.First().Value.Time == 1000);
}
[Test]
public void TestFinalFrameInBundleHasHeader()
{
FrameDataBundle lastBundle = null;
AddStep("bind to client", () => spectatorClient.OnNewFrames += (_, bundle) => lastBundle = bundle);
start(-1234);
sendFrames();
finish();
AddUntilStep("bundle received", () => lastBundle != null);
AddAssert("first frame does not have header", () => lastBundle.Frames[0].Header == null);
AddAssert("last frame has header", () => lastBundle.Frames[^1].Header != null);
}
private OsuFramedReplayInputHandler replayHandler =>
(OsuFramedReplayInputHandler)Stack.ChildrenOfType<OsuInputManager>().First().ReplayInputHandler;

View File

@ -221,7 +221,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
}
public override void CollectPendingInputs(List<IInput> inputs)
protected override void CollectReplayInputs(List<IInput> inputs)
{
inputs.Add(new MousePositionAbsoluteInput { Position = GamefieldToScreenSpace(CurrentFrame?.Position ?? Vector2.Zero) });
inputs.Add(new ReplayState<TestAction> { PressedActions = CurrentFrame?.Actions ?? new List<TestAction>() });

View File

@ -197,7 +197,24 @@ namespace osu.Game.Tests.Visual.SongSelect
Accuracy = 1,
MaxCombo = 244,
TotalScore = 1707827,
//Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
Mods = new Mod[]
{
new OsuModHidden(),
new OsuModHardRock(),
new OsuModFlashlight
{
FollowDelay = { Value = 200 },
SizeMultiplier = { Value = 5 },
},
new OsuModDifficultyAdjust
{
CircleSize = { Value = 11 },
ApproachRate = { Value = 10 },
OverallDifficulty = { Value = 10 },
DrainRate = { Value = 10 },
ExtendedLimits = { Value = true }
}
},
Ruleset = new OsuRuleset().RulesetInfo,
BeatmapInfo = beatmapInfo,
User = new APIUser
@ -217,7 +234,7 @@ namespace osu.Game.Tests.Visual.SongSelect
Accuracy = 1,
MaxCombo = 244,
TotalScore = 1707827,
//Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
Ruleset = new OsuRuleset().RulesetInfo,
User = new APIUser
@ -237,7 +254,7 @@ namespace osu.Game.Tests.Visual.SongSelect
Accuracy = 1,
MaxCombo = 244,
TotalScore = 1707827,
//Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
Ruleset = new OsuRuleset().RulesetInfo,
@ -258,7 +275,7 @@ namespace osu.Game.Tests.Visual.SongSelect
Accuracy = 1,
MaxCombo = 244,
TotalScore = 1707827,
//Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
Ruleset = new OsuRuleset().RulesetInfo,
@ -279,7 +296,7 @@ namespace osu.Game.Tests.Visual.SongSelect
Accuracy = 1,
MaxCombo = 244,
TotalScore = 1707827,
//Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
Ruleset = new OsuRuleset().RulesetInfo,
@ -300,7 +317,7 @@ namespace osu.Game.Tests.Visual.SongSelect
Accuracy = 0.9826,
MaxCombo = 244,
TotalScore = 1707827,
//Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
Ruleset = new OsuRuleset().RulesetInfo,
@ -321,7 +338,7 @@ namespace osu.Game.Tests.Visual.SongSelect
Accuracy = 0.9654,
MaxCombo = 244,
TotalScore = 1707827,
//Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
Ruleset = new OsuRuleset().RulesetInfo,
@ -342,7 +359,7 @@ namespace osu.Game.Tests.Visual.SongSelect
Accuracy = 0.6025,
MaxCombo = 244,
TotalScore = 1707827,
//Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
Ruleset = new OsuRuleset().RulesetInfo,
@ -363,7 +380,7 @@ namespace osu.Game.Tests.Visual.SongSelect
Accuracy = 0.5140,
MaxCombo = 244,
TotalScore = 1707827,
//Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
Ruleset = new OsuRuleset().RulesetInfo,
@ -384,7 +401,7 @@ namespace osu.Game.Tests.Visual.SongSelect
Accuracy = 0.4222,
MaxCombo = 244,
TotalScore = 1707827,
//Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
Ruleset = new OsuRuleset().RulesetInfo,

View File

@ -46,7 +46,7 @@ namespace osu.Game.Beatmaps
public BeatmapMetadata(RealmUser? user = null)
{
Author = new RealmUser();
Author = user ?? new RealmUser();
}
[UsedImplicitly] // Realm

View File

@ -30,6 +30,7 @@ namespace osu.Game.Beatmaps.Drawables
{
background = new Box
{
Alpha = 0.9f,
RelativeSizeAxes = Axes.Both
},
new FillFlowContainer

View File

@ -63,7 +63,7 @@ namespace osu.Game.Database
return;
}
perform(retrieveFromID(r, ID));
perform(retrieveFromID(r));
RealmLiveStatistics.USAGE_ASYNC.Value++;
});
}
@ -85,7 +85,7 @@ namespace osu.Game.Database
return realm.Run(r =>
{
var returnData = perform(retrieveFromID(r, ID));
var returnData = perform(retrieveFromID(r));
RealmLiveStatistics.USAGE_ASYNC.Value++;
if (returnData is RealmObjectBase realmObject && realmObject.IsManaged)
@ -139,11 +139,11 @@ namespace osu.Game.Database
}
dataIsFromUpdateThread = true;
data = retrieveFromID(realm.Realm, ID);
data = retrieveFromID(realm.Realm);
RealmLiveStatistics.USAGE_UPDATE_REFETCH.Value++;
}
private T retrieveFromID(Realm realm, Guid id)
private T retrieveFromID(Realm realm)
{
var found = realm.Find<T>(ID);

View File

@ -10,6 +10,8 @@ using osu.Framework.Graphics.Sprites;
using osu.Game.Configuration;
using System;
using JetBrains.Annotations;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Events;
@ -30,13 +32,17 @@ namespace osu.Game.Graphics.Cursor
private DragRotationState dragRotationState;
private Vector2 positionMouseDown;
private Sample tapSample;
[BackgroundDependencyLoader(true)]
private void load([NotNull] OsuConfigManager config, [CanBeNull] ScreenshotManager screenshotManager)
private void load([NotNull] OsuConfigManager config, [CanBeNull] ScreenshotManager screenshotManager, AudioManager audio)
{
cursorRotate = config.GetBindable<bool>(OsuSetting.CursorRotation);
if (screenshotManager != null)
screenshotCursorVisibility.BindTo(screenshotManager.CursorVisibility);
tapSample = audio.Samples.Get(@"UI/cursor-tap");
}
protected override bool OnMouseMove(MouseMoveEvent e)
@ -87,6 +93,8 @@ namespace osu.Game.Graphics.Cursor
dragRotationState = DragRotationState.DragStarted;
positionMouseDown = e.MousePosition;
}
playTapSample();
}
return base.OnMouseDown(e);
@ -104,6 +112,9 @@ namespace osu.Game.Graphics.Cursor
activeCursor.RotateTo(0, 600 * (1 + Math.Abs(activeCursor.Rotation / 720)), Easing.OutElasticHalf);
dragRotationState = DragRotationState.NotDragging;
}
if (State.Value == Visibility.Visible)
playTapSample(0.8);
}
base.OnMouseUp(e);
@ -121,6 +132,18 @@ namespace osu.Game.Graphics.Cursor
activeCursor.ScaleTo(0.6f, 250, Easing.In);
}
private void playTapSample(double baseFrequency = 1f)
{
const float random_range = 0.02f;
SampleChannel channel = tapSample.GetChannel();
// Scale to [-0.75, 0.75] so that the sample isn't fully panned left or right (sounds weird)
channel.Balance.Value = ((activeCursor.X / DrawWidth) * 2 - 1) * 0.75;
channel.Frequency.Value = baseFrequency - (random_range / 2f) + RNG.NextDouble(random_range);
channel.Play();
}
public class Cursor : Container
{
private Container cursorContainer;

View File

@ -9,6 +9,7 @@ using osu.Framework.Input.StateChanges;
using osu.Framework.Input.StateChanges.Events;
using osu.Framework.Input.States;
using osu.Framework.Platform;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.UI;
using osuTK;
@ -79,5 +80,38 @@ namespace osu.Game.Input.Handlers
PressedActions = pressedActions;
}
}
/// <summary>
/// An <see cref="IInput"/> that is triggered when a frame containing replay statistics arrives.
/// </summary>
public class ReplayStatisticsFrameInput : IInput
{
/// <summary>
/// The frame containing the statistics.
/// </summary>
public ReplayFrame Frame;
public void Apply(InputState state, IInputStateChangeHandler handler)
{
handler.HandleInputStateChange(new ReplayStatisticsFrameEvent(state, this, Frame));
}
}
/// <summary>
/// An <see cref="InputStateChangeEvent"/> that is triggered when a frame containing replay statistics arrives.
/// </summary>
public class ReplayStatisticsFrameEvent : InputStateChangeEvent
{
/// <summary>
/// The frame containing the statistics.
/// </summary>
public readonly ReplayFrame Frame;
public ReplayStatisticsFrameEvent(InputState state, IInput input, ReplayFrame frame)
: base(state, input)
{
Frame = frame;
}
}
}
}

View File

@ -32,7 +32,7 @@ using osu.Game.Utils;
namespace osu.Game.Online.Leaderboards
{
public class LeaderboardScore : OsuClickableContainer, IHasContextMenu
public class LeaderboardScore : OsuClickableContainer, IHasContextMenu, IHasCustomTooltip<ScoreInfo>
{
public const float HEIGHT = 60;
@ -70,6 +70,9 @@ namespace osu.Game.Online.Leaderboards
[Resolved]
private Storage storage { get; set; }
public ITooltip<ScoreInfo> GetCustomTooltip() => new LeaderboardScoreTooltip();
public virtual ScoreInfo TooltipContent => Score;
public LeaderboardScore(ScoreInfo score, int? rank, bool isOnlineScope = true)
{
Score = score;
@ -183,7 +186,6 @@ namespace osu.Game.Online.Leaderboards
Anchor = Anchor.BottomLeft,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10f, 0f),
Margin = new MarginPadding { Left = edge_margin },
Children = statisticsLabels
},
@ -228,7 +230,6 @@ namespace osu.Game.Online.Leaderboards
Origin = Anchor.BottomRight,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(1),
ChildrenEnumerable = Score.Mods.Select(mod => new ModIcon(mod) { Scale = new Vector2(0.375f) })
},
},
@ -313,6 +314,7 @@ namespace osu.Game.Online.Leaderboards
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Padding = new MarginPadding { Right = 10 },
Children = new Drawable[]
{
new Container

View File

@ -0,0 +1,219 @@
// 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;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Game.Scoring;
using osuTK;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Framework.Allocation;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
#nullable enable
namespace osu.Game.Online.Leaderboards
{
public class LeaderboardScoreTooltip : VisibilityContainer, ITooltip<ScoreInfo>
{
private OsuSpriteText timestampLabel = null!;
private FillFlowContainer<HitResultCell> topScoreStatistics = null!;
private FillFlowContainer<HitResultCell> bottomScoreStatistics = null!;
private FillFlowContainer<ModCell> modStatistics = null!;
public LeaderboardScoreTooltip()
{
AutoSizeAxes = Axes.Both;
AutoSizeDuration = 200;
AutoSizeEasing = Easing.OutQuint;
Masking = true;
CornerRadius = 5;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
InternalChildren = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0.9f,
Colour = colours.Gray3,
},
new FillFlowContainer
{
Margin = new MarginPadding(5),
Spacing = new Vector2(10),
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
// Info row
timestampLabel = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold),
},
// Mods row
modStatistics = new FillFlowContainer<ModCell>
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(5, 0),
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
// Actual stats rows
topScoreStatistics = new FillFlowContainer<HitResultCell>
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10, 0),
},
bottomScoreStatistics = new FillFlowContainer<HitResultCell>
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10, 0),
},
}
},
}
}
};
}
private ScoreInfo? displayedScore;
public void SetContent(ScoreInfo score)
{
if (displayedScore?.Equals(score) == true)
return;
displayedScore = score;
timestampLabel.Text = $"Played on {score.Date.ToLocalTime():d MMMM yyyy HH:mm}";
modStatistics.Clear();
topScoreStatistics.Clear();
bottomScoreStatistics.Clear();
foreach (var mod in score.Mods)
{
modStatistics.Add(new ModCell(mod));
}
foreach (var result in score.GetStatisticsForDisplay())
{
if (result.Result > HitResult.Perfect)
bottomScoreStatistics.Add(new HitResultCell(result));
else
topScoreStatistics.Add(new HitResultCell(result));
}
}
protected override void PopIn() => this.FadeIn(20, Easing.OutQuint);
protected override void PopOut() => this.FadeOut(80, Easing.OutQuint);
public void Move(Vector2 pos) => Position = pos;
private class HitResultCell : CompositeDrawable
{
private readonly string displayName;
private readonly HitResult result;
private readonly int count;
public HitResultCell(HitResultDisplayStatistic stat)
{
AutoSizeAxes = Axes.Both;
displayName = stat.DisplayName;
result = stat.Result;
count = stat.Count;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
InternalChild = new FillFlowContainer
{
Height = 12,
AutoSizeAxes = Axes.X,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5f, 0f),
Children = new Drawable[]
{
new OsuSpriteText
{
Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold),
Text = displayName.ToUpperInvariant(),
Colour = colours.ForHitResult(result),
},
new OsuSpriteText
{
Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold),
Text = count.ToString(),
},
}
};
}
}
private class ModCell : CompositeDrawable
{
private readonly Mod mod;
public ModCell(Mod mod)
{
AutoSizeAxes = Axes.Both;
this.mod = mod;
}
[BackgroundDependencyLoader]
private void load()
{
FillFlowContainer container;
InternalChild = container = new FillFlowContainer
{
Height = 15,
AutoSizeAxes = Axes.X,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(2f, 0f),
Children = new Drawable[]
{
new ModIcon(mod, showTooltip: false).With(icon =>
{
icon.Origin = Anchor.CentreLeft;
icon.Anchor = Anchor.CentreLeft;
icon.Scale = new Vector2(15f / icon.Height);
}),
}
};
string description = mod.SettingDescription;
if (!string.IsNullOrEmpty(description))
{
container.Add(new OsuSpriteText
{
RelativeSizeAxes = Axes.Y,
Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold),
Text = mod.SettingDescription,
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Margin = new MarginPadding { Top = 1 },
});
}
}
}
}
}

View File

@ -115,6 +115,9 @@ namespace osu.Game.Online.Spectator
Task ISpectatorClient.UserSentFrames(int userId, FrameDataBundle data)
{
if (data.Frames.Count > 0)
data.Frames[^1].Header = data.Header;
Schedule(() => OnNewFrames?.Invoke(userId, data));
return Task.CompletedTask;

View File

@ -247,7 +247,7 @@ namespace osu.Game
var defaultBeatmap = new DummyWorkingBeatmap(Audio, Textures);
// ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup()
dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, realm, Scheduler, Host, () => difficultyCache, LocalConfig));
dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, realm, Scheduler, () => difficultyCache, LocalConfig));
dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, realm, RulesetStore, API, Audio, Resources, Host, defaultBeatmap, performOnlineLookups: true));
dependencies.Cache(BeatmapDownloader = new BeatmapModelDownloader(BeatmapManager, API));

View File

@ -6,6 +6,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Input.StateChanges;
using osu.Game.Input.Handlers;
using osu.Game.Replays;
@ -174,5 +175,19 @@ namespace osu.Game.Rulesets.Replays
return Frames[index].Time;
}
public sealed override void CollectPendingInputs(List<IInput> inputs)
{
base.CollectPendingInputs(inputs);
CollectReplayInputs(inputs);
if (CurrentFrame?.Header != null)
inputs.Add(new ReplayStatisticsFrameInput { Frame = CurrentFrame });
}
protected virtual void CollectReplayInputs(List<IInput> inputs)
{
}
}
}

View File

@ -1,16 +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.
#nullable enable
using MessagePack;
using osu.Game.Online.Spectator;
namespace osu.Game.Rulesets.Replays
{
[MessagePackObject]
public class ReplayFrame
{
/// <summary>
/// The time at which this <see cref="ReplayFrame"/> takes place.
/// </summary>
[Key(0)]
public double Time;
/// <summary>
/// A <see cref="FrameHeader"/> containing the state of a play after this <see cref="ReplayFrame"/> takes place.
/// May be omitted where exact per-frame accuracy is not required.
/// </summary>
[IgnoreMember]
public FrameHeader? Header;
public ReplayFrame()
{
}

View File

@ -149,6 +149,10 @@ namespace osu.Game.Rulesets
var instanceInfo = (Activator.CreateInstance(resolvedType) as Ruleset)?.RulesetInfo
?? throw new RulesetLoadException(@"Instantiation failure");
// If a ruleset isn't up-to-date with the API, it could cause a crash at an arbitrary point of execution.
// To eagerly handle cases of missing implementations, enumerate all types here and mark as non-available on throw.
resolvedType.Assembly.GetTypes();
r.Name = instanceInfo.Name;
r.ShortName = instanceInfo.ShortName;
r.InstantiationInfo = instanceInfo.InstantiationInfo;

View File

@ -176,7 +176,7 @@ namespace osu.Game.Rulesets.Scoring
/// <summary>
/// An array of all scorable <see cref="HitResult"/>s.
/// </summary>
public static readonly HitResult[] SCORABLE_TYPES = ((HitResult[])Enum.GetValues(typeof(HitResult))).Where(r => r.IsScorable()).ToArray();
public static readonly HitResult[] ALL_TYPES = ((HitResult[])Enum.GetValues(typeof(HitResult))).ToArray();
/// <summary>
/// Whether a <see cref="HitResult"/> is valid within a given <see cref="HitResult"/> range.

View File

@ -8,6 +8,7 @@ using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Replays;
namespace osu.Game.Rulesets.Scoring
{
@ -107,6 +108,25 @@ namespace osu.Game.Rulesets.Scoring
JudgedHits = 0;
}
/// <summary>
/// Reset all statistics based on header information contained within a replay frame.
/// </summary>
/// <remarks>
/// If the provided replay frame does not have any header information, this will be a noop.
/// </remarks>
/// <param name="ruleset">The ruleset to be used for retrieving statistics.</param>
/// <param name="frame">The replay frame to read header statistics from.</param>
public virtual void ResetFromReplayFrame(Ruleset ruleset, ReplayFrame frame)
{
if (frame.Header == null)
return;
JudgedHits = 0;
foreach ((_, int count) in frame.Header.Statistics)
JudgedHits += count;
}
/// <summary>
/// Creates the <see cref="JudgementResult"/> that represents the scoring result for a <see cref="HitObject"/>.
/// </summary>

View File

@ -7,9 +7,11 @@ using System.Diagnostics;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Utils;
using osu.Game.Extensions;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Replays;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.Scoring
@ -18,6 +20,11 @@ namespace osu.Game.Rulesets.Scoring
{
private const double max_score = 1000000;
/// <summary>
/// Invoked when this <see cref="ScoreProcessor"/> was reset from a replay frame.
/// </summary>
public event Action OnResetFromReplayFrame;
/// <summary>
/// The current total score.
/// </summary>
@ -125,6 +132,8 @@ namespace osu.Game.Rulesets.Scoring
if (result.FailedAtJudgement)
return;
scoreResultCounts[result.Type] = scoreResultCounts.GetValueOrDefault(result.Type) + 1;
if (!result.Type.IsScorable())
return;
@ -151,8 +160,6 @@ namespace osu.Game.Rulesets.Scoring
rollingMaxBaseScore += result.Judgement.MaxNumericResult;
}
scoreResultCounts[result.Type] = scoreResultCounts.GetValueOrDefault(result.Type) + 1;
hitEvents.Add(CreateHitEvent(result));
lastHitObject = result.HitObject;
@ -175,6 +182,8 @@ namespace osu.Game.Rulesets.Scoring
if (result.FailedAtJudgement)
return;
scoreResultCounts[result.Type] = scoreResultCounts.GetValueOrDefault(result.Type) - 1;
if (!result.Type.IsScorable())
return;
@ -186,8 +195,6 @@ namespace osu.Game.Rulesets.Scoring
rollingMaxBaseScore -= result.Judgement.MaxNumericResult;
}
scoreResultCounts[result.Type] = scoreResultCounts.GetValueOrDefault(result.Type) - 1;
Debug.Assert(hitEvents.Count > 0);
lastHitObject = hitEvents[^1].LastHitObject;
hitEvents.RemoveAt(hitEvents.Count - 1);
@ -329,12 +336,6 @@ namespace osu.Game.Rulesets.Scoring
HighestCombo.Value = 0;
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
hitEvents.Clear();
}
/// <summary>
/// Retrieve a score populated with data for the current play this processor is responsible for.
/// </summary>
@ -346,11 +347,72 @@ namespace osu.Game.Rulesets.Scoring
score.Accuracy = Accuracy.Value;
score.Rank = Rank.Value;
foreach (var result in HitResultExtensions.SCORABLE_TYPES)
foreach (var result in HitResultExtensions.ALL_TYPES)
score.Statistics[result] = GetStatistic(result);
score.HitEvents = hitEvents;
}
/// <summary>
/// Maximum <see cref="HitResult"/> for a normal hit (i.e. not tick/bonus) for this ruleset. Only populated via <see cref="ResetFromReplayFrame"/>.
/// </summary>
private HitResult? maxNormalResult;
public override void ResetFromReplayFrame(Ruleset ruleset, ReplayFrame frame)
{
base.ResetFromReplayFrame(ruleset, frame);
if (frame.Header == null)
return;
baseScore = 0;
rollingMaxBaseScore = 0;
HighestCombo.Value = frame.Header.MaxCombo;
foreach ((HitResult result, int count) in frame.Header.Statistics)
{
// Bonus scores are counted separately directly from the statistics dictionary later on.
if (!result.IsScorable() || result.IsBonus())
continue;
// The maximum result of this judgement if it wasn't a miss.
// E.g. For a GOOD judgement, the max result is either GREAT/PERFECT depending on which one the ruleset uses (osu!: GREAT, osu!mania: PERFECT).
HitResult maxResult;
switch (result)
{
case HitResult.LargeTickHit:
case HitResult.LargeTickMiss:
maxResult = HitResult.LargeTickHit;
break;
case HitResult.SmallTickHit:
case HitResult.SmallTickMiss:
maxResult = HitResult.SmallTickHit;
break;
default:
maxResult = maxNormalResult ??= ruleset.GetHitResults().OrderByDescending(kvp => Judgement.ToNumericResult(kvp.result)).First().result;
break;
}
baseScore += count * Judgement.ToNumericResult(result);
rollingMaxBaseScore += count * Judgement.ToNumericResult(maxResult);
}
scoreResultCounts.Clear();
scoreResultCounts.AddRange(frame.Header.Statistics);
updateScore();
OnResetFromReplayFrame?.Invoke();
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
hitEvents.Clear();
}
}
public enum ScoringMode

View File

@ -16,6 +16,7 @@ using osu.Game.Configuration;
using osu.Game.Input;
using osu.Game.Input.Bindings;
using osu.Game.Input.Handlers;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play;
using static osu.Game.Input.Handlers.ReplayInputHandler;
@ -26,6 +27,11 @@ namespace osu.Game.Rulesets.UI
{
public readonly KeyBindingContainer<T> KeyBindingContainer;
private readonly Ruleset ruleset;
[Resolved(CanBeNull = true)]
private ScoreProcessor scoreProcessor { get; set; }
private ReplayRecorder recorder;
public ReplayRecorder Recorder
@ -51,6 +57,8 @@ namespace osu.Game.Rulesets.UI
protected RulesetInputManager(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique)
{
this.ruleset = ruleset.CreateInstance();
InternalChild = KeyBindingContainer =
CreateKeyBindingContainer(ruleset, variant, unique)
.WithChild(content = new Container { RelativeSizeAxes = Axes.Both });
@ -66,17 +74,23 @@ namespace osu.Game.Rulesets.UI
public override void HandleInputStateChange(InputStateChangeEvent inputStateChange)
{
if (inputStateChange is ReplayStateChangeEvent<T> replayStateChanged)
switch (inputStateChange)
{
foreach (var action in replayStateChanged.ReleasedActions)
KeyBindingContainer.TriggerReleased(action);
case ReplayStateChangeEvent<T> stateChangeEvent:
foreach (var action in stateChangeEvent.ReleasedActions)
KeyBindingContainer.TriggerReleased(action);
foreach (var action in replayStateChanged.PressedActions)
KeyBindingContainer.TriggerPressed(action);
}
else
{
base.HandleInputStateChange(inputStateChange);
foreach (var action in stateChangeEvent.PressedActions)
KeyBindingContainer.TriggerPressed(action);
break;
case ReplayStatisticsFrameEvent statisticsStateChangeEvent:
scoreProcessor?.ResetFromReplayFrame(ruleset, statisticsStateChangeEvent.Frame);
break;
default:
base.HandleInputStateChange(inputStateChange);
break;
}
}

View File

@ -32,7 +32,7 @@ namespace osu.Game.Scoring
private readonly ScoreModelManager scoreModelManager;
public ScoreManager(RulesetStore rulesets, Func<BeatmapManager> beatmaps, Storage storage, RealmAccess realm, Scheduler scheduler,
IIpcHost importHost = null, Func<BeatmapDifficultyCache> difficulties = null, OsuConfigManager configManager = null)
Func<BeatmapDifficultyCache> difficulties = null, OsuConfigManager configManager = null)
{
this.realm = realm;
this.scheduler = scheduler;

View File

@ -51,7 +51,7 @@ namespace osu.Game.Screens.Edit.Timing
}
Columns = createHeaders();
Content = value.Select((g, i) => createContent(i, g)).ToArray().ToRectangular();
Content = value.Select(createContent).ToArray().ToRectangular();
}
}
@ -76,7 +76,7 @@ namespace osu.Game.Screens.Edit.Timing
return columns.ToArray();
}
private Drawable[] createContent(int index, ControlPointGroup group)
private Drawable[] createContent(ControlPointGroup group)
{
return new Drawable[]
{

View File

@ -14,6 +14,8 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components
{
private readonly APIUserScoreAggregate score;
public override ScoreInfo TooltipContent => null; // match aggregate scores can't show statistics that the custom tooltip displays.
public MatchLeaderboardScore(APIUserScoreAggregate score, int? rank, bool isOnlineScope = true)
: base(score.CreateScoreInfo(), rank, isOnlineScope)
{

View File

@ -10,7 +10,6 @@ using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osuTK;
@ -49,7 +48,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
private void load()
{
InternalChild = new FillFlowContainer
{
@ -127,7 +126,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
}
};
createColourBars(colours);
createColourBars();
}
protected override void LoadComplete()
@ -150,7 +149,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
iconLate.Rotation = -Rotation;
}
private void createColourBars(OsuColour colours)
private void createColourBars()
{
var windows = HitWindows.GetAllAvailableWindows().ToArray();

View File

@ -109,7 +109,7 @@ namespace osu.Game.Screens.Play.HUD
return;
if (isRolling)
onDisplayedCountRolling(displayedCount, value);
onDisplayedCountRolling(value);
else if (displayedCount + 1 == value)
onDisplayedCountIncrement(value);
else
@ -151,7 +151,7 @@ namespace osu.Game.Screens.Play.HUD
if (prev + 1 == Current.Value)
onCountIncrement(prev, Current.Value);
else
onCountChange(prev, Current.Value);
onCountChange(Current.Value);
}
else
{
@ -226,7 +226,7 @@ namespace osu.Game.Screens.Play.HUD
transformRoll(currentValue, newValue);
}
private void onCountChange(int currentValue, int newValue)
private void onCountChange(int newValue)
{
scheduledPopOutCurrentId++;
@ -236,7 +236,7 @@ namespace osu.Game.Screens.Play.HUD
DisplayedCount = newValue;
}
private void onDisplayedCountRolling(int currentValue, int newValue)
private void onDisplayedCountRolling(int newValue)
{
if (newValue == 0)
displayedCountSpriteText.FadeOut(fade_out_duration);

View File

@ -163,6 +163,7 @@ namespace osu.Game.Screens.Play
PrepareReplay();
ScoreProcessor.NewJudgement += result => ScoreProcessor.PopulateScore(Score.ScoreInfo);
ScoreProcessor.OnResetFromReplayFrame += () => ScoreProcessor.PopulateScore(Score.ScoreInfo);
gameActive.BindValueChanged(_ => updatePauseOnFocusLostState(), true);
}
@ -243,7 +244,7 @@ namespace osu.Game.Screens.Play
{
// underlay and gameplay should have access to the skinning sources.
createUnderlayComponents(),
createGameplayComponents(Beatmap.Value, playableBeatmap)
createGameplayComponents(Beatmap.Value)
}
},
FailOverlay = new FailOverlay
@ -356,7 +357,7 @@ namespace osu.Game.Screens.Play
private Drawable createUnderlayComponents() =>
DimmableStoryboard = new DimmableStoryboard(Beatmap.Value.Storyboard) { RelativeSizeAxes = Axes.Both };
private Drawable createGameplayComponents(IWorkingBeatmap working, IBeatmap playableBeatmap) => new ScalingContainer(ScalingMode.Gameplay)
private Drawable createGameplayComponents(IWorkingBeatmap working) => new ScalingContainer(ScalingMode.Gameplay)
{
Children = new Drawable[]
{

View File

@ -72,6 +72,7 @@ namespace osu.Game.Screens.Play
var convertedFrame = (ReplayFrame)convertibleFrame;
convertedFrame.Time = frame.Time;
convertedFrame.Header = frame.Header;
score.Replay.Frames.Add(convertedFrame);
}

View File

@ -77,7 +77,7 @@ namespace osu.Game.Skinning
userFiles = new StorageBackedResourceStore(storage.GetStorageForDirectory("files"));
skinModelManager = new SkinModelManager(storage, realm, host, this);
skinModelManager = new SkinModelManager(storage, realm, this);
var defaultSkins = new[]
{

View File

@ -27,7 +27,7 @@ namespace osu.Game.Skinning
private readonly IStorageResourceProvider skinResources;
public SkinModelManager(Storage storage, RealmAccess realm, GameHost host, IStorageResourceProvider skinResources)
public SkinModelManager(Storage storage, RealmAccess realm, IStorageResourceProvider skinResources)
: base(storage, realm)
{
this.skinResources = skinResources;

View File

@ -126,7 +126,7 @@ namespace osu.Game.Tests.Visual
protected override BeatmapModelManager CreateBeatmapModelManager(Storage storage, RealmAccess realm, RulesetStore rulesets, BeatmapOnlineLookupQueue onlineLookupQueue)
{
return new TestBeatmapModelManager(storage, realm, rulesets, onlineLookupQueue);
return new TestBeatmapModelManager(storage, realm, onlineLookupQueue);
}
protected override WorkingBeatmapCache CreateWorkingBeatmapCache(AudioManager audioManager, IResourceStore<byte[]> resources, IResourceStore<byte[]> storage, WorkingBeatmap defaultBeatmap, GameHost host)
@ -150,7 +150,7 @@ namespace osu.Game.Tests.Visual
internal class TestBeatmapModelManager : BeatmapModelManager
{
public TestBeatmapModelManager(Storage storage, RealmAccess databaseAccess, RulesetStore rulesetStore, BeatmapOnlineLookupQueue beatmapOnlineLookupQueue)
public TestBeatmapModelManager(Storage storage, RealmAccess databaseAccess, BeatmapOnlineLookupQueue beatmapOnlineLookupQueue)
: base(databaseAccess, storage, beatmapOnlineLookupQueue)
{
}

View File

@ -37,7 +37,7 @@
</PackageReference>
<PackageReference Include="Realm" Version="10.8.0" />
<PackageReference Include="ppy.osu.Framework" Version="2022.128.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.115.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.202.0" />
<PackageReference Include="Sentry" Version="3.13.0" />
<PackageReference Include="SharpCompress" Version="0.30.1" />
<PackageReference Include="NUnit" Version="3.13.2" />

View File

@ -61,7 +61,7 @@
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2022.128.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.115.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.202.0" />
</ItemGroup>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
<PropertyGroup>