1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-20 16:00:46 +08:00

Compare commits

...

307 Commits

239 changed files with 4295 additions and 1573 deletions
+9 -15
View File
@@ -11,6 +11,10 @@ body:
- Current open `priority:0` issues, filterable [here](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3Apriority%3A0).
- And most importantly, search for your issue both in the [issue listing](https://github.com/ppy/osu/issues) and the [Q&A discussion listing](https://github.com/ppy/osu/discussions/categories/q-a). If you find that it already exists, respond with a reaction or add any further information that may be helpful.
# ATTENTION LINUX USERS
If you are having an issue and it is hardware related, **please open a [q&a discussion](https://github.com/ppy/osu/discussions/categories/q-a)** instead of an issue. There's a high chance your issue is due to your system configuration, and not our software.
- type: dropdown
attributes:
label: Type
@@ -46,22 +50,16 @@ body:
value: |
## Logs
Attaching log files is required for every reported bug. See instructions below on how to find them.
**Logs are reset when you reopen the game.** If the game crashed or has been closed since you found the bug, retrieve the logs using the file explorer instead.
Attaching log files is required for **every** issue, regardless of whether you deem them required or not. See instructions below on how to find them.
### Desktop platforms
If the game has not yet been closed since you found the bug:
1. Head on to game settings and click on "Open osu! folder"
2. Then open the `logs` folder located there
1. Head on to game settings and click on "Export logs"
2. Click the notification to locate the file
3. Drag the generated `.zip` files into the github issue window
The default places to find the logs on desktop platforms are as follows:
- `%AppData%/osu/logs` *on Windows*
- `~/.local/share/osu/logs` *on Linux*
- `~/Library/Application Support/osu/logs` *on macOS*
If you have selected a custom location for the game files, you can find the `logs` folder there.
![export logs button](https://github.com/ppy/osu/assets/191335/cbfa5550-b7ed-4c5c-8dd0-8b87cc90ad9b)
### Mobile platforms
@@ -69,10 +67,6 @@ body:
- *On Android*, navigate to `Android/data/sh.ppy.osulazer/files/logs` using a file browser app.
- *On iOS*, connect your device to a PC and copy the `logs` directory from the app's document storage using iTunes. (https://support.apple.com/en-us/HT201301#copy-to-computer)
---
After locating the `logs` folder, select all log files inside and drag them into the "Logs" box below.
- type: textarea
attributes:
label: Logs
+1 -1
View File
@@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.1219.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.1227.1" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.
@@ -11,7 +11,7 @@ using osuTK;
namespace osu.Game.Rulesets.Catch.Tests.Mods
{
public partial class TestSceneCatchModPerfect : ModPerfectTestScene
public partial class TestSceneCatchModPerfect : ModFailConditionTestScene
{
protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
@@ -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.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osuTK;
namespace osu.Game.Rulesets.Catch.Tests
{
public partial class TestSceneOutOfBoundsObjects : TestSceneCatchPlayer
{
protected override bool Autoplay => true;
[Test]
public void TestNoOutOfBoundsObjects()
{
bool anyObjectOutOfBounds = false;
AddStep("reset flag", () => anyObjectOutOfBounds = false);
AddUntilStep("check for out-of-bounds objects",
() =>
{
anyObjectOutOfBounds |= Player.ChildrenOfType<DrawableCatchHitObject>().Any(dho => dho.X < 0 || dho.X > CatchPlayfield.WIDTH);
return Player.ScoreProcessor.HasCompleted.Value;
});
AddAssert("no out of bound objects found", () => !anyObjectOutOfBounds);
}
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap
{
BeatmapInfo = new BeatmapInfo
{
Ruleset = ruleset,
},
HitObjects = new List<HitObject>
{
new Fruit { StartTime = 1000, X = -50 },
new Fruit { StartTime = 1200, X = CatchPlayfield.WIDTH + 50 },
new JuiceStream
{
StartTime = 1500,
X = 10,
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(-200, 0)
})
},
new JuiceStream
{
StartTime = 3000,
X = CatchPlayfield.WIDTH - 10,
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(200, 0)
})
},
}
};
}
}
@@ -7,7 +7,7 @@ using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Catch.Scoring;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
@@ -18,6 +18,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty
{
internal class CatchLegacyScoreSimulator : ILegacyScoreSimulator
{
private readonly ScoreProcessor scoreProcessor = new CatchScoreProcessor();
private int legacyBonusScore;
private int standardisedBonusScore;
private int combo;
@@ -134,7 +136,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
if (isBonus)
{
legacyBonusScore += scoreIncrease;
standardisedBonusScore += Judgement.ToNumericResult(bonusResult);
standardisedBonusScore += scoreProcessor.GetBaseScoreForResult(bonusResult);
}
else
attributes.AccuracyScore += scoreIncrease;
@@ -3,6 +3,7 @@
using System;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
@@ -17,7 +18,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
private double placementStartTime;
private double placementEndTime;
protected override bool IsValidForPlacement => HitObject.Duration > 0;
protected override bool IsValidForPlacement => Precision.DefinitelyBigger(HitObject.Duration, 0);
public BananaShowerPlacementBlueprint()
{
@@ -4,6 +4,7 @@
using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
@@ -24,7 +25,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
private InputManager inputManager = null!;
protected override bool IsValidForPlacement => HitObject.Duration > 0;
protected override bool IsValidForPlacement => Precision.DefinitelyBigger(HitObject.Duration, 0);
public JuiceStreamPlacementBlueprint()
{
@@ -1,10 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Catch.UI;
using osuTK;
using osuTK.Graphics;
@@ -70,7 +72,10 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
private void updateXPosition(ValueChangedEvent<float> _)
{
X = OriginalXBindable.Value + XOffsetBindable.Value;
// same as `CatchHitObject.EffectiveX`.
// not using that property directly to support scenarios where `HitObject` may not necessarily be present
// for this pooled drawable.
X = Math.Clamp(OriginalXBindable.Value + XOffsetBindable.Value, 0, CatchPlayfield.WIDTH);
}
protected override void OnApply()
@@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Catch.Scoring
}
protected override double GetComboScoreChange(JudgementResult result)
=> Judgement.ToNumericResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(combo_cap, combo_base));
=> GetBaseScoreForResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(combo_cap, combo_base));
public override ScoreRank RankFromAccuracy(double accuracy)
{
@@ -3,6 +3,7 @@
using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mania.Objects;
@@ -25,8 +26,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
public void TestHitWindowWithoutDoubleTime() => CreateModTest(new ModTestData
{
PassCondition = () => Player.ScoreProcessor.JudgedHits > 0
&& Player.ScoreProcessor.Accuracy.Value == 1
&& Player.ScoreProcessor.TotalScore.Value == 1_000_000,
&& Precision.AlmostEquals(Player.ScoreProcessor.Accuracy.Value, 0.9836, 0.01)
&& Player.ScoreProcessor.TotalScore.Value == 946_049,
Autoplay = false,
Beatmap = new Beatmap
{
@@ -53,7 +54,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
Mod = doubleTime,
PassCondition = () => Player.ScoreProcessor.JudgedHits > 0
&& Player.ScoreProcessor.Accuracy.Value == 1
&& Player.ScoreProcessor.TotalScore.Value == (long)(1_000_010 * doubleTime.ScoreMultiplier),
&& Player.ScoreProcessor.TotalScore.Value == (long)(1_000_000 * doubleTime.ScoreMultiplier),
Autoplay = false,
Beatmap = new Beatmap
{
@@ -1,14 +1,19 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Replays;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests.Mods
{
public partial class TestSceneManiaModPerfect : ModPerfectTestScene
public partial class TestSceneManiaModPerfect : ModFailConditionTestScene
{
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
@@ -24,5 +29,52 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
[TestCase(false)]
[TestCase(true)]
public void TestHoldNote(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new HoldNote { StartTime = 1000, EndTime = 3000 }), shouldMiss);
[Test]
public void TestGreatHit() => CreateModTest(new ModTestData
{
Mod = new ManiaModPerfect(),
PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(false),
Autoplay = false,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new Note
{
StartTime = 1000,
}
},
},
ReplayFrames = new List<ReplayFrame>
{
new ManiaReplayFrame(1020, ManiaAction.Key1),
new ManiaReplayFrame(2000)
}
});
[Test]
public void TestBreakOnHoldNote() => CreateModTest(new ModTestData
{
Mod = new ManiaModPerfect(),
PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(true) && Player.Results.Count == 2,
Autoplay = false,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new HoldNote
{
StartTime = 1000,
EndTime = 3000,
},
},
},
ReplayFrames = new List<ReplayFrame>
{
new ManiaReplayFrame(1000, ManiaAction.Key1),
new ManiaReplayFrame(2000)
}
});
}
}
@@ -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.Collections.Generic;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Replays;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests.Mods
{
public partial class TestSceneManiaModSuddenDeath : ModFailConditionTestScene
{
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
public TestSceneManiaModSuddenDeath()
: base(new ManiaModSuddenDeath())
{
}
[Test]
public void TestGreatHit() => CreateModTest(new ModTestData
{
Mod = new ManiaModSuddenDeath(),
PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(false),
Autoplay = false,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new Note
{
StartTime = 1000,
}
},
},
ReplayFrames = new List<ReplayFrame>
{
new ManiaReplayFrame(1020, ManiaAction.Key1),
new ManiaReplayFrame(2000)
}
});
[Test]
public void TestBreakOnHoldNote() => CreateModTest(new ModTestData
{
Mod = new ManiaModSuddenDeath(),
PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(true) && Player.Results.Count == 2,
Autoplay = false,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new HoldNote
{
StartTime = 1000,
EndTime = 3000,
},
},
},
ReplayFrames = new List<ReplayFrame>
{
new ManiaReplayFrame(1000, ManiaAction.Key1),
new ManiaReplayFrame(2000)
}
});
}
}
@@ -200,12 +200,10 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Perfect);
// judgement combo offset by perfect bonus judgement. see logic in DrawableNote.CheckForResult.
assertComboAtJudgement(1, 1);
assertComboAtJudgement(0, 1);
assertTailJudgement(HitResult.Meh);
assertComboAtJudgement(2, 0);
// judgement combo offset by perfect bonus judgement. see logic in DrawableNote.CheckForResult.
assertComboAtJudgement(4, 1);
assertComboAtJudgement(1, 0);
assertComboAtJudgement(3, 1);
}
/// <summary>
@@ -54,7 +54,7 @@ namespace osu.Game.Rulesets.Mania.Tests
AddAssert("all objects perfectly judged",
() => judgementResults.Select(result => result.Type),
() => Is.EquivalentTo(judgementResults.Select(result => result.Judgement.MaxResult)));
AddAssert("score is correct", () => currentPlayer.ScoreProcessor.TotalScore.Value, () => Is.EqualTo(1_000_030));
AddAssert("score is correct", () => currentPlayer.ScoreProcessor.TotalScore.Value, () => Is.EqualTo(1_000_000));
}
[Test]
@@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Mania.Tests
AddAssert("all objects perfectly judged",
() => judgementResults.Select(result => result.Type),
() => Is.EquivalentTo(judgementResults.Select(result => result.Judgement.MaxResult)));
AddAssert("score is correct", () => currentPlayer.ScoreProcessor.TotalScore.Value, () => Is.EqualTo(1_000_040));
AddAssert("score is correct", () => currentPlayer.ScoreProcessor.TotalScore.Value, () => Is.EqualTo(1_000_000));
}
private void performTest(List<ManiaHitObject> hitObjects, List<ReplayFrame> frames)
@@ -59,23 +59,26 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
{
double roundedCircleSize = Math.Round(difficulty.CircleSize);
if (new ManiaRuleset().RulesetInfo.Equals(difficulty.SourceRuleset))
if (difficulty.SourceRuleset.ShortName == ManiaRuleset.SHORT_NAME)
return (int)Math.Max(1, roundedCircleSize);
double roundedOverallDifficulty = Math.Round(difficulty.OverallDifficulty);
int countSliderOrSpinner = difficulty.EndTimeObjectCount;
if (difficulty.TotalObjectCount > 0 && difficulty.EndTimeObjectCount >= 0)
{
int countSliderOrSpinner = difficulty.EndTimeObjectCount;
// In osu!stable, this division appears as if it happens on floats, but due to release-mode
// optimisations, it actually ends up happening on doubles.
double percentSpecialObjects = (double)countSliderOrSpinner / difficulty.TotalObjectCount;
// In osu!stable, this division appears as if it happens on floats, but due to release-mode
// optimisations, it actually ends up happening on doubles.
double percentSpecialObjects = (double)countSliderOrSpinner / difficulty.TotalObjectCount;
if (percentSpecialObjects < 0.2)
return 7;
if (percentSpecialObjects < 0.3 || roundedCircleSize >= 5)
return roundedOverallDifficulty > 5 ? 7 : 6;
if (percentSpecialObjects > 0.6)
return roundedOverallDifficulty > 4 ? 5 : 4;
if (percentSpecialObjects < 0.2)
return 7;
if (percentSpecialObjects < 0.3 || roundedCircleSize >= 5)
return roundedOverallDifficulty > 5 ? 7 : 6;
if (percentSpecialObjects > 0.6)
return roundedOverallDifficulty > 4 ? 5 : 4;
}
return Math.Max(4, Math.Min((int)roundedOverallDifficulty + 1, 7));
}
@@ -5,6 +5,7 @@ using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
using osu.Game.Rulesets.Mania.Objects;
@@ -23,7 +24,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
[Resolved]
private IScrollingInfo scrollingInfo { get; set; } = null!;
protected override bool IsValidForPlacement => HitObject.Duration > 0;
protected override bool IsValidForPlacement => Precision.DefinitelyBigger(HitObject.Duration, 0);
public HoldNotePlacementBlueprint()
: base(new HoldNote())
@@ -10,5 +10,10 @@ namespace osu.Game.Rulesets.Mania.Mods
public class ManiaModDoubleTime : ModDoubleTime, IManiaRateAdjustmentMod
{
public HitWindows HitWindows { get; set; } = new ManiaHitWindows();
// For now, all rate-increasing mods should be given a 1x multiplier in mania because it doesn't always
// make the map harder and is more of a personal preference.
// In the future, we can consider adjusting this by experimenting with not applying the hitwindow leniency.
public override double ScoreMultiplier => 1;
}
}
@@ -11,5 +11,10 @@ namespace osu.Game.Rulesets.Mania.Mods
public class ManiaModNightcore : ModNightcore<ManiaHitObject>, IManiaRateAdjustmentMod
{
public HitWindows HitWindows { get; set; } = new ManiaHitWindows();
// For now, all rate-increasing mods should be given a 1x multiplier in mania because it doesn't always
// make the map any harder and is more of a personal preference.
// In the future, we can consider adjusting this by experimenting with not applying the hitwindow leniency.
public override double ScoreMultiplier => 1;
}
}
@@ -1,11 +1,26 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModPerfect : ModPerfect
{
protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result)
{
if (!isRelevantResult(result.Judgement.MinResult) && !isRelevantResult(result.Judgement.MaxResult) && !isRelevantResult(result.Type))
return false;
// Mania allows imperfect "Great" hits without failing.
if (result.Judgement.MaxResult == HitResult.Perfect)
return result.Type < HitResult.Great;
return result.Type != result.Judgement.MaxResult;
}
private bool isRelevantResult(HitResult result) => result.AffectsAccuracy() || result.AffectsCombo();
}
}
@@ -13,8 +13,6 @@ using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.Skinning.Default;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit;
@@ -40,8 +38,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
private Drawable headPiece;
private DrawableNotePerfectBonus perfectBonus;
public DrawableNote()
: this(null)
{
@@ -93,10 +89,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
if (!userTriggered)
{
if (!HitObject.HitWindows.CanBeHit(timeOffset))
{
perfectBonus.TriggerResult(false);
ApplyResult(r => r.Type = r.Judgement.MinResult);
}
return;
}
@@ -107,16 +100,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
result = GetCappedResult(result);
perfectBonus.TriggerResult(result == HitResult.Perfect);
ApplyResult(r => r.Type = result);
}
public override void MissForcefully()
{
perfectBonus.TriggerResult(false);
base.MissForcefully();
}
/// <summary>
/// Some objects in mania may want to limit the max result.
/// </summary>
@@ -137,32 +123,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
}
protected override void AddNestedHitObject(DrawableHitObject hitObject)
{
switch (hitObject)
{
case DrawableNotePerfectBonus bonus:
AddInternal(perfectBonus = bonus);
break;
}
}
protected override void ClearNestedHitObjects()
{
RemoveInternal(perfectBonus, false);
}
protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
{
switch (hitObject)
{
case NotePerfectBonus bonus:
return new DrawableNotePerfectBonus(bonus);
}
return base.CreateNestedHitObject(hitObject);
}
private void updateSnapColour()
{
if (beatmap == null || HitObject == null) return;
@@ -1,26 +0,0 @@
// 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.
namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
public partial class DrawableNotePerfectBonus : DrawableManiaHitObject<NotePerfectBonus>
{
public override bool DisplayResult => false;
public DrawableNotePerfectBonus()
: this(null!)
{
}
public DrawableNotePerfectBonus(NotePerfectBonus hitObject)
: base(hitObject)
{
}
/// <summary>
/// Apply a judgement result.
/// </summary>
/// <param name="hit">Whether this tick was reached.</param>
internal void TriggerResult(bool hit) => ApplyResult(r => r.Type = hit ? r.Judgement.MaxResult : r.Judgement.MinResult);
}
}
-8
View File
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Threading;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Judgements;
@@ -13,12 +12,5 @@ namespace osu.Game.Rulesets.Mania.Objects
public class Note : ManiaHitObject
{
public override Judgement CreateJudgement() => new ManiaJudgement();
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{
base.CreateNestedHitObjects(cancellationToken);
AddNested(new NotePerfectBonus { StartTime = StartTime });
}
}
}
@@ -1,20 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Judgements;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Objects
{
public class NotePerfectBonus : ManiaHitObject
{
public override Judgement CreateJudgement() => new NotePerfectBonusJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
public class NotePerfectBonusJudgement : ManiaJudgement
{
public override HitResult MaxResult => HitResult.SmallBonus;
}
}
}
@@ -26,13 +26,37 @@ namespace osu.Game.Rulesets.Mania.Scoring
protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion)
{
return 10000 * comboProgress
+ 990000 * Math.Pow(Accuracy.Value, 2 + 2 * Accuracy.Value) * accuracyProgress
return 150000 * comboProgress
+ 850000 * Math.Pow(Accuracy.Value, 2 + 2 * Accuracy.Value) * accuracyProgress
+ bonusPortion;
}
protected override double GetComboScoreChange(JudgementResult result)
=> Judgement.ToNumericResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(400, combo_base));
{
return getBaseComboScoreForResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(400, combo_base));
}
public override int GetBaseScoreForResult(HitResult result)
{
switch (result)
{
case HitResult.Perfect:
return 305;
}
return base.GetBaseScoreForResult(result);
}
private int getBaseComboScoreForResult(HitResult result)
{
switch (result)
{
case HitResult.Perfect:
return 300;
}
return GetBaseScoreForResult(result);
}
private class JudgementOrderComparer : IComparer<HitObject>
{
-1
View File
@@ -109,7 +109,6 @@ namespace osu.Game.Rulesets.Mania.UI
TopLevelContainer.Add(HitObjectArea.Explosions.CreateProxy());
RegisterPool<Note, DrawableNote>(10, 50);
RegisterPool<NotePerfectBonus, DrawableNotePerfectBonus>(10, 50);
RegisterPool<HoldNote, DrawableHoldNote>(10, 50);
RegisterPool<HeadNote, DrawableHoldNoteHead>(10, 50);
RegisterPool<TailNote, DrawableHoldNoteTail>(10, 50);
@@ -19,12 +19,14 @@ using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Mania.Skinning;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Scoring;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania.UI
{
@@ -57,6 +59,9 @@ namespace osu.Game.Rulesets.Mania.UI
// Stores the current speed adjustment active in gameplay.
private readonly Track speedAdjustmentTrack = new TrackVirtual(0);
[Resolved]
private ISkinSource skin { get; set; }
public DrawableManiaRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
: base(ruleset, beatmap, mods)
{
@@ -104,7 +109,20 @@ namespace osu.Game.Rulesets.Mania.UI
updateTimeRange();
}
private void updateTimeRange() => TimeRange.Value = smoothTimeRange * speedAdjustmentTrack.AggregateTempo.Value * speedAdjustmentTrack.AggregateFrequency.Value;
private void updateTimeRange()
{
float hitPosition = skin.GetConfig<ManiaSkinConfigurationLookup, float>(
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.HitPosition))?.Value
?? Stage.HIT_TARGET_POSITION;
const float length_to_default_hit_position = 768 - LegacyManiaSkinConfiguration.DEFAULT_HIT_POSITION;
float lengthToHitPosition = 768 - hitPosition;
// This scaling factor preserves the scroll speed as the scroll length varies from changes to the hit position.
float scale = lengthToHitPosition / length_to_default_hit_position;
TimeRange.Value = smoothTimeRange * speedAdjustmentTrack.AggregateTempo.Value * speedAdjustmentTrack.AggregateFrequency.Value * scale;
}
/// <summary>
/// Computes a scroll time (in milliseconds) from a scroll speed in the range of 1-40.
@@ -1,17 +1,21 @@
// 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.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Replays;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public partial class TestSceneOsuModPerfect : ModPerfectTestScene
public partial class TestSceneOsuModPerfect : ModFailConditionTestScene
{
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
@@ -50,5 +54,30 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
CreateHitObjectTest(new HitObjectTestData(spinner), shouldMiss);
}
[Test]
public void TestMissSliderTail() => CreateModTest(new ModTestData
{
Mod = new OsuModPerfect(),
PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(true),
Autoplay = false,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new Slider
{
Position = new Vector2(256, 192),
StartTime = 1000,
Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(100, 0), })
},
},
},
ReplayFrames = new List<ReplayFrame>
{
new OsuReplayFrame(1000, new Vector2(256, 192), OsuAction.LeftButton),
new OsuReplayFrame(1001, new Vector2(256, 192)),
}
});
}
}
@@ -0,0 +1,53 @@
// 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.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 partial class TestSceneOsuModStrictTracking : OsuModTestScene
{
[Test]
public void TestSliderInput() => CreateModTest(new ModTestData
{
Mod = new OsuModStrictTracking(),
Autoplay = false,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new Slider
{
StartTime = 1000,
Path = new SliderPath
{
ControlPoints =
{
new PathControlPoint(),
new PathControlPoint(new Vector2(0, 100))
}
}
}
}
},
ReplayFrames = new List<ReplayFrame>
{
new OsuReplayFrame(0, new Vector2(), OsuAction.LeftButton),
new OsuReplayFrame(500, new Vector2(200, 0), OsuAction.LeftButton),
new OsuReplayFrame(501, new Vector2(200, 0)),
new OsuReplayFrame(1000, new Vector2(), OsuAction.LeftButton),
new OsuReplayFrame(1750, new Vector2(0, 100), OsuAction.LeftButton),
new OsuReplayFrame(1751, new Vector2(0, 100)),
},
PassCondition = () => Player.ScoreProcessor.Combo.Value == 2
});
}
}
@@ -0,0 +1,77 @@
// 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.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Replays;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public partial class TestSceneOsuModSuddenDeath : ModFailConditionTestScene
{
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
public TestSceneOsuModSuddenDeath()
: base(new OsuModSuddenDeath())
{
}
[Test]
public void TestMissTail() => CreateModTest(new ModTestData
{
Mod = new OsuModSuddenDeath(),
PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(false),
Autoplay = false,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new Slider
{
Position = new Vector2(256, 192),
StartTime = 1000,
Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(100, 0), })
},
},
},
ReplayFrames = new List<ReplayFrame>
{
new OsuReplayFrame(1000, new Vector2(256, 192), OsuAction.LeftButton),
new OsuReplayFrame(1001, new Vector2(256, 192)),
}
});
[Test]
public void TestMissTick() => CreateModTest(new ModTestData
{
Mod = new OsuModSuddenDeath(),
PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(true),
Autoplay = false,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new Slider
{
Position = new Vector2(256, 192),
StartTime = 1000,
Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(200, 0), })
},
},
},
ReplayFrames = new List<ReplayFrame>
{
new OsuReplayFrame(1000, new Vector2(256, 192), OsuAction.LeftButton),
new OsuReplayFrame(1001, new Vector2(256, 192)),
}
});
}
}
@@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Screens;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Replays;
using osu.Game.Rulesets.Judgements;
@@ -160,6 +161,10 @@ namespace osu.Game.Rulesets.Osu.Tests
Position = new Vector2(256 - slider_path_length / 2, 192),
TickDistanceMultiplier = 3,
ClassicSliderBehaviour = classic,
Samples = new[]
{
new HitSampleInfo(HitSampleInfo.HIT_NORMAL)
},
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
@@ -58,10 +58,7 @@ namespace osu.Game.Rulesets.Osu.Tests
double trackerRotationTolerance = 0;
addSeekStep(5000);
AddStep("calculate rotation tolerance", () =>
{
trackerRotationTolerance = Math.Abs(drawableSpinner.RotationTracker.Rotation * 0.1f);
});
AddStep("calculate rotation tolerance", () => { trackerRotationTolerance = Math.Abs(drawableSpinner.RotationTracker.Rotation * 0.1f); });
AddAssert("is disc rotation not almost 0", () => drawableSpinner.RotationTracker.Rotation, () => Is.Not.EqualTo(0).Within(100));
AddAssert("is disc rotation absolute not almost 0", () => drawableSpinner.Result.TotalRotation, () => Is.Not.EqualTo(0).Within(100));
@@ -133,9 +130,11 @@ namespace osu.Game.Rulesets.Osu.Tests
AddAssert("player score matching expected bonus score", () =>
{
var scoreProcessor = ((ScoreExposedPlayer)Player).ScoreProcessor;
// multipled by 2 to nullify the score multiplier. (autoplay mod selected)
long totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2;
return totalScore == (int)(drawableSpinner.Result.TotalRotation / 360) * new SpinnerTick().CreateJudgement().MaxNumericResult;
long totalScore = scoreProcessor.TotalScore.Value * 2;
return totalScore == (int)(drawableSpinner.Result.TotalRotation / 360) * scoreProcessor.GetBaseScoreForResult(new SpinnerTick().CreateJudgement().MaxResult);
});
addSeekStep(0);
@@ -5,12 +5,12 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Scoring.Legacy;
@@ -18,6 +18,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{
internal class OsuLegacyScoreSimulator : ILegacyScoreSimulator
{
private readonly ScoreProcessor scoreProcessor = new OsuScoreProcessor();
private int legacyBonusScore;
private int standardisedBonusScore;
private int combo;
@@ -171,7 +173,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (isBonus)
{
legacyBonusScore += scoreIncrease;
standardisedBonusScore += Judgement.ToNumericResult(bonusResult);
standardisedBonusScore += scoreProcessor.GetBaseScoreForResult(bonusResult);
}
else
attributes.AccuracyScore += scoreIncrease;
@@ -17,6 +17,7 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
@@ -54,7 +55,7 @@ namespace osu.Game.Rulesets.Osu.Edit
.Concat(DistanceSnapProvider.CreateTernaryButtons())
.Concat(new[]
{
new TernaryButton(rectangularGridSnapToggle, "Grid Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Th })
new TernaryButton(rectangularGridSnapToggle, "Grid Snap", () => new SpriteIcon { Icon = OsuIcon.EditorGridSnap })
});
private BindableList<HitObject> selectedHitObjects;
@@ -1,14 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModAccuracyChallenge : ModAccuracyChallenge
{
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).ToArray();
}
}
@@ -16,7 +16,7 @@ using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModAutopilot : Mod, IApplicableFailOverride, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>
public class OsuModAutopilot : Mod, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>
{
public override string Name => "Autopilot";
public override string Acronym => "AP";
@@ -29,18 +29,12 @@ namespace osu.Game.Rulesets.Osu.Mods
{
typeof(OsuModSpunOut),
typeof(ModRelax),
typeof(ModFailCondition),
typeof(ModNoFail),
typeof(ModAutoplay),
typeof(OsuModMagnetised),
typeof(OsuModRepel),
typeof(ModTouchDevice)
};
public bool PerformFail() => false;
public bool RestartOnFail => false;
private OsuInputManager inputManager = null!;
private List<OsuReplayFrame> replayFrames = null!;
+162
View File
@@ -0,0 +1,162 @@
// 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.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI;
using osuTK;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModDepth : ModWithVisibilityAdjustment, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>
{
public override string Name => "Depth";
public override string Acronym => "DP";
public override IconUsage? Icon => FontAwesome.Solid.Cube;
public override ModType Type => ModType.Fun;
public override LocalisableString Description => "3D. Almost.";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModRepel), typeof(OsuModFreezeFrame), typeof(ModWithVisibilityAdjustment) }).ToArray();
private static readonly Vector3 camera_position = new Vector3(OsuPlayfield.BASE_SIZE.X * 0.5f, OsuPlayfield.BASE_SIZE.Y * 0.5f, -200);
private readonly float sliderMinDepth = depthForScale(1.5f); // Depth at which slider's scale will be 1.5f
[SettingSource("Maximum depth", "How far away objects appear.", 0)]
public BindableFloat MaxDepth { get; } = new BindableFloat(100)
{
Precision = 10,
MinValue = 50,
MaxValue = 200
};
[SettingSource("Show Approach Circles", "Whether approach circles should be visible.", 1)]
public BindableBool ShowApproachCircles { get; } = new BindableBool(true);
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) => applyTransform(hitObject, state);
protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state) => applyTransform(hitObject, state);
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{
// Hide judgment displays and follow points as they won't make any sense.
// Judgements can potentially be turned on in a future where they display at a position relative to their drawable counterpart.
drawableRuleset.Playfield.DisplayJudgements.Value = false;
(drawableRuleset.Playfield as OsuPlayfield)?.FollowPoints.Hide();
}
private void applyTransform(DrawableHitObject drawable, ArmedState state)
{
switch (drawable)
{
case DrawableHitCircle circle:
if (!ShowApproachCircles.Value)
{
var hitObject = (OsuHitObject)drawable.HitObject;
double appearTime = hitObject.StartTime - hitObject.TimePreempt;
using (circle.BeginAbsoluteSequence(appearTime))
circle.ApproachCircle.Hide();
}
break;
}
}
public void Update(Playfield playfield)
{
double time = playfield.Time.Current;
foreach (var drawable in playfield.HitObjectContainer.AliveObjects)
{
switch (drawable)
{
case DrawableHitCircle circle:
processHitObject(time, circle);
break;
case DrawableSlider slider:
processSlider(time, slider);
break;
}
}
}
private void processHitObject(double time, DrawableOsuHitObject drawable)
{
var hitObject = drawable.HitObject;
// Circles are always moving at the constant speed. They'll fade out before reaching the camera even at extreme conditions (AR 11, max depth).
double speed = MaxDepth.Value / hitObject.TimePreempt;
double appearTime = hitObject.StartTime - hitObject.TimePreempt;
float z = MaxDepth.Value - (float)((Math.Max(time, appearTime) - appearTime) * speed);
float scale = scaleForDepth(z);
drawable.Position = toPlayfieldPosition(scale, hitObject.StackedPosition);
drawable.Scale = new Vector2(scale);
}
private void processSlider(double time, DrawableSlider drawableSlider)
{
var hitObject = drawableSlider.HitObject;
double baseSpeed = MaxDepth.Value / hitObject.TimePreempt;
double appearTime = hitObject.StartTime - hitObject.TimePreempt;
// Allow slider to move at a constant speed if its scale at the end time will be lower than 1.5f
float zEnd = MaxDepth.Value - (float)((Math.Max(hitObject.StartTime + hitObject.Duration, appearTime) - appearTime) * baseSpeed);
if (zEnd > sliderMinDepth)
{
processHitObject(time, drawableSlider);
return;
}
double offsetAfterStartTime = hitObject.Duration + 500;
double slowSpeed = Math.Min(-sliderMinDepth / offsetAfterStartTime, baseSpeed);
double decelerationTime = hitObject.TimePreempt * 0.2;
float decelerationDistance = (float)(decelerationTime * (baseSpeed + slowSpeed) * 0.5);
float z;
if (time < hitObject.StartTime - decelerationTime)
{
float fullDistance = decelerationDistance + (float)(baseSpeed * (hitObject.TimePreempt - decelerationTime));
z = fullDistance - (float)((Math.Max(time, appearTime) - appearTime) * baseSpeed);
}
else if (time < hitObject.StartTime)
{
double timeOffset = time - (hitObject.StartTime - decelerationTime);
double deceleration = (slowSpeed - baseSpeed) / decelerationTime;
z = decelerationDistance - (float)(baseSpeed * timeOffset + deceleration * timeOffset * timeOffset * 0.5);
}
else
{
double endTime = hitObject.StartTime + offsetAfterStartTime;
z = -(float)((Math.Min(time, endTime) - hitObject.StartTime) * slowSpeed);
}
float scale = scaleForDepth(z);
drawableSlider.Position = toPlayfieldPosition(scale, hitObject.StackedPosition);
drawableSlider.Scale = new Vector2(scale);
}
private static float scaleForDepth(float depth) => -camera_position.Z / Math.Max(1f, depth - camera_position.Z);
private static float depthForScale(float scale) => -camera_position.Z / scale + camera_position.Z;
private static Vector2 toPlayfieldPosition(float scale, Vector2 positionAtZeroDepth)
{
return (positionAtZeroDepth - camera_position.Xy) * scale + camera_position.Xy;
}
}
}
@@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override LocalisableString Description => "Burn the notes into your memory.";
//Alters the transforms of the approach circles, breaking the effects of these mods.
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModApproachDifferent), typeof(OsuModTransform) }).ToArray();
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModApproachDifferent), typeof(OsuModTransform), typeof(OsuModDepth) }).ToArray();
public override ModType Type => ModType.Fun;
+1 -1
View File
@@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override LocalisableString Description => @"Play with no approach circles and fading circles/sliders.";
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;
public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn) };
public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn), typeof(OsuModDepth) };
public const double FADE_IN_DURATION_MULTIPLIER = 0.4;
public const double FADE_OUT_DURATION_MULTIPLIER = 0.3;
@@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Fun;
public override LocalisableString Description => "No need to chase the circles your cursor is a magnet!";
public override double ScoreMultiplier => 0.5;
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax), typeof(OsuModRepel), typeof(OsuModBubbles) };
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax), typeof(OsuModRepel), typeof(OsuModBubbles), typeof(OsuModDepth) };
[SettingSource("Attraction strength", "How strong the pull is.", 0)]
public BindableFloat AttractionStrength { get; } = new BindableFloat(0.5f)
@@ -1,14 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModNoFail : ModNoFail
{
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).ToArray();
}
}
@@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Mods
protected virtual float EndScale => 1;
public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn), typeof(OsuModObjectScaleTween) };
public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn), typeof(OsuModObjectScaleTween), typeof(OsuModDepth) };
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
{
@@ -1,14 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModPerfect : ModPerfect
{
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot) }).ToArray();
}
}
+1 -1
View File
@@ -88,7 +88,7 @@ namespace osu.Game.Rulesets.Osu.Mods
if (!slider.HeadCircle.IsHit)
handleHitCircle(slider.HeadCircle);
requiresHold |= slider.Ball.IsHovered || h.IsHovered;
requiresHold |= slider.SliderInputManager.IsMouseInFollowArea(true);
break;
case DrawableSpinner spinner:
+1 -1
View File
@@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Fun;
public override LocalisableString Description => "Hit objects run away!";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModMagnetised), typeof(OsuModBubbles) };
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModMagnetised), typeof(OsuModBubbles), typeof(OsuModDepth) };
[SettingSource("Repulsion strength", "How strong the repulsion is.", 0)]
public BindableFloat RepulsionStrength { get; } = new BindableFloat(0.5f)
+1 -1
View File
@@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods
// todo: this mod needs to be incompatible with "hidden" due to forcing the circle to remain opaque,
// further implementation will be required for supporting that.
public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModObjectScaleTween), typeof(OsuModHidden) };
public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModObjectScaleTween), typeof(OsuModHidden), typeof(OsuModDepth) };
private const int rotate_offset = 360;
private const float rotate_starting_width = 2;
@@ -36,6 +36,9 @@ namespace osu.Game.Rulesets.Osu.Mods
{
if (e.NewValue || slider.Judged) return;
if (slider.Time.Current < slider.HitObject.StartTime)
return;
var tail = slider.NestedHitObjects.OfType<StrictTrackingDrawableSliderTail>().First();
if (!tail.Judged)
@@ -11,7 +11,6 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[]
{
typeof(OsuModAutopilot),
typeof(OsuModTargetPractice),
}).ToArray();
}
@@ -47,7 +47,8 @@ namespace osu.Game.Rulesets.Osu.Mods
typeof(OsuModRandom),
typeof(OsuModSpunOut),
typeof(OsuModStrictTracking),
typeof(OsuModSuddenDeath)
typeof(OsuModSuddenDeath),
typeof(OsuModDepth)
}).ToArray();
[SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(SettingsNumberBox))]
@@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override LocalisableString Description => "Put your faith in the approach circles...";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles) };
public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles), typeof(OsuModDepth) };
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
{
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Fun;
public override LocalisableString Description => "Everything rotates. EVERYTHING.";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModWiggle), typeof(OsuModMagnetised), typeof(OsuModRepel), typeof(OsuModFreezeFrame) }).ToArray();
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModWiggle), typeof(OsuModMagnetised), typeof(OsuModRepel), typeof(OsuModFreezeFrame), typeof(OsuModDepth) }).ToArray();
private float theta;
+1 -1
View File
@@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Fun;
public override LocalisableString Description => "They just won't stay still...";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModTransform), typeof(OsuModMagnetised), typeof(OsuModRepel) };
public override Type[] IncompatibleMods => new[] { typeof(OsuModTransform), typeof(OsuModMagnetised), typeof(OsuModRepel), typeof(OsuModDepth) };
private const int wiggle_duration = 100; // (ms) Higher = fewer wiggles
@@ -128,8 +128,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
foreach (var drawableHitObject in NestedHitObjects)
drawableHitObject.AccentColour.Value = colour.NewValue;
}, true);
Tracking.BindValueChanged(updateSlidingSample);
}
protected override void OnApply()
@@ -166,14 +164,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
slidingSample?.Stop();
}
private void updateSlidingSample(ValueChangedEvent<bool> tracking)
{
if (tracking.NewValue)
slidingSample?.Play();
else
slidingSample?.Stop();
}
protected override void AddNestedHitObject(DrawableHitObject hitObject)
{
base.AddNestedHitObject(hitObject);
@@ -238,9 +228,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Tracking.Value = SliderInputManager.Tracking;
if (Tracking.Value && slidingSample != null)
// keep the sliding sample playing at the current tracking position
slidingSample.Balance.Value = CalculateSamplePlaybackBalance(CalculateDrawableRelativePosition(Ball));
if (slidingSample != null)
{
if (Tracking.Value && Time.Current >= HitObject.StartTime)
{
// keep the sliding sample playing at the current tracking position
if (!slidingSample.IsPlaying)
slidingSample.Play();
slidingSample.Balance.Value = CalculateSamplePlaybackBalance(CalculateDrawableRelativePosition(Ball));
}
else if (slidingSample.IsPlaying)
slidingSample.Stop();
}
double completionProgress = Math.Clamp((Time.Current - HitObject.StartTime) / HitObject.Duration, 0, 1);
@@ -34,8 +34,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private Drawable scaleContainer;
public override bool DisplayResult => false;
public DrawableSliderRepeat()
: base(null)
{
@@ -24,11 +24,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
protected DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject;
/// <summary>
/// The judgement text is provided by the <see cref="DrawableSlider"/>.
/// </summary>
public override bool DisplayResult => false;
/// <summary>
/// Whether the hit samples only play on successful hits.
/// If <c>false</c>, the hit samples will also play on misses.
@@ -20,8 +20,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private const float default_tick_size = 16;
public override bool DisplayResult => false;
protected DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject;
private SkinnableDrawable scaleContainer;
@@ -17,6 +17,7 @@ using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Rulesets.Scoring;
@@ -46,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private const float spinning_sample_initial_frequency = 1.0f;
private const float spinning_sample_modulated_base_frequency = 0.5f;
private SkinnableSound maxBonusSample;
private PausableSkinnableSound maxBonusSample;
/// <summary>
/// The amount of bonus score gained from spinning after the required number of spins, for display purposes.
@@ -113,7 +114,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Looping = true,
Frequency = { Value = spinning_sample_initial_frequency }
},
maxBonusSample = new SkinnableSound
maxBonusSample = new PausableSkinnableSound
{
MinimumSampleVolume = MINIMUM_SAMPLE_VOLUME,
}
@@ -312,7 +313,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
updateBonusScore();
}
private static readonly int score_per_tick = new SpinnerBonusTick.OsuSpinnerBonusTickJudgement().MaxNumericResult;
private static readonly int score_per_tick = new OsuScoreProcessor().GetBaseScoreForResult(new SpinnerBonusTick.OsuSpinnerBonusTickJudgement().MaxResult);
private void updateBonusScore()
{
@@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
protected override void Update()
{
base.Update();
updateTracking(isMouseInFollowArea(Tracking));
updateTracking(IsMouseInFollowArea(Tracking));
}
public void PostProcessHeadJudgement(DrawableSliderHead head)
@@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (!head.Judged || !head.Result.IsHit)
return;
if (!isMouseInFollowArea(true))
if (!IsMouseInFollowArea(true))
return;
Debug.Assert(screenSpaceMousePosition != null);
@@ -129,7 +129,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
// If all ticks were hit so far, enable tracking the full extent.
// If any ticks were missed, assume tracking would've broken at some point, and should only activate if the cursor is within the slider ball.
// For the second case, this may be the last chance we have to enable tracking before other objects get judged, otherwise the same would normally happen via Update().
updateTracking(allTicksInRange || isMouseInFollowArea(false));
updateTracking(allTicksInRange || IsMouseInFollowArea(false));
}
public void TryJudgeNestedObject(DrawableOsuHitObject nestedObject, double timeOffset)
@@ -174,7 +174,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
/// Whether the mouse is currently in the follow area.
/// </summary>
/// <param name="expanded">Whether to test against the maximum area of the follow circle.</param>
private bool isMouseInFollowArea(bool expanded)
public bool IsMouseInFollowArea(bool expanded)
{
if (screenSpaceMousePosition is not Vector2 pos)
return false;
+4 -1
View File
@@ -48,6 +48,8 @@ namespace osu.Game.Rulesets.Osu
public override ScoreProcessor CreateScoreProcessor() => new OsuScoreProcessor();
public override HealthProcessor CreateHealthProcessor(double drainStartTime) => new OsuHealthProcessor(drainStartTime);
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new OsuBeatmapConverter(beatmap, this);
public override IBeatmapProcessor CreateBeatmapProcessor(IBeatmap beatmap) => new OsuBeatmapProcessor(beatmap);
@@ -209,7 +211,8 @@ namespace osu.Game.Rulesets.Osu
new ModAdaptiveSpeed(),
new OsuModFreezeFrame(),
new OsuModBubbles(),
new OsuModSynesthesia()
new OsuModSynesthesia(),
new OsuModDepth()
};
case ModType.System:
@@ -0,0 +1,127 @@
// 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 osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Scoring
{
public partial class OsuHealthProcessor : DrainingHealthProcessor
{
private ComboResult currentComboResult = ComboResult.Perfect;
public OsuHealthProcessor(double drainStartTime, double drainLenience = 0)
: base(drainStartTime, drainLenience)
{
}
protected override double GetHealthIncreaseFor(JudgementResult result)
{
if (IsSimulating)
return getHealthIncreaseFor(result);
if (result.HitObject is not IHasComboInformation combo)
return getHealthIncreaseFor(result);
if (combo.NewCombo)
currentComboResult = ComboResult.Perfect;
switch (result.Type)
{
case HitResult.LargeTickMiss:
case HitResult.Ok:
setComboResult(ComboResult.Good);
break;
case HitResult.Meh:
case HitResult.Miss:
setComboResult(ComboResult.None);
break;
}
// The slider tail has a special judgement that can't accurately be described above.
if (result.HitObject is SliderTailCircle && !result.IsHit)
setComboResult(ComboResult.Good);
if (combo.LastInCombo && result.Type.IsHit())
{
switch (currentComboResult)
{
case ComboResult.Perfect:
return getHealthIncreaseFor(result) + 0.07;
case ComboResult.Good:
return getHealthIncreaseFor(result) + 0.05;
default:
return getHealthIncreaseFor(result) + 0.03;
}
}
return getHealthIncreaseFor(result);
void setComboResult(ComboResult comboResult) => currentComboResult = (ComboResult)Math.Min((int)currentComboResult, (int)comboResult);
}
protected override void Reset(bool storeResults)
{
base.Reset(storeResults);
currentComboResult = ComboResult.Perfect;
}
private double getHealthIncreaseFor(JudgementResult result)
{
switch (result.Type)
{
case HitResult.SmallTickMiss:
return IBeatmapDifficultyInfo.DifficultyRange(Beatmap.Difficulty.DrainRate, -0.02, -0.075, -0.14);
case HitResult.LargeTickMiss:
return IBeatmapDifficultyInfo.DifficultyRange(Beatmap.Difficulty.DrainRate, -0.02, -0.075, -0.14);
case HitResult.Miss:
return IBeatmapDifficultyInfo.DifficultyRange(Beatmap.Difficulty.DrainRate, -0.03, -0.125, -0.2);
case HitResult.SmallTickHit:
// When classic slider mechanics are enabled, this result comes from the tail.
return 0.02;
case HitResult.LargeTickHit:
switch (result.HitObject)
{
case SliderTick:
return 0.015;
case SliderHeadCircle:
case SliderTailCircle:
case SliderRepeat:
return 0.02;
}
break;
case HitResult.Meh:
return 0.002;
case HitResult.Ok:
return 0.011;
case HitResult.Great:
return 0.03;
case HitResult.SmallBonus:
return 0.0085;
case HitResult.LargeBonus:
return 0.01;
}
return base.GetHealthIncreaseFor(result);
}
}
}
@@ -62,25 +62,32 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
/// </remarks>
public virtual void PlayAnimation()
{
switch (Result)
if (Result == HitResult.IgnoreMiss || Result == HitResult.LargeTickMiss)
{
default:
JudgementText
.FadeInFromZero(300, Easing.OutQuint)
.ScaleTo(Vector2.One)
.ScaleTo(new Vector2(1.2f), 1800, Easing.OutQuint);
break;
this.RotateTo(-45);
this.ScaleTo(1.8f);
this.ScaleTo(1.2f, 100, Easing.In);
case HitResult.Miss:
this.ScaleTo(1.6f);
this.ScaleTo(1, 100, Easing.In);
this.MoveTo(Vector2.Zero);
this.MoveToOffset(new Vector2(0, 10), 800, Easing.InQuint);
}
else if (Result.IsMiss())
{
this.ScaleTo(1.6f);
this.ScaleTo(1, 100, Easing.In);
this.MoveTo(Vector2.Zero);
this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
this.MoveTo(Vector2.Zero);
this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
this.RotateTo(0);
this.RotateTo(40, 800, Easing.InQuint);
break;
this.RotateTo(0);
this.RotateTo(40, 800, Easing.InQuint);
}
else
{
JudgementText
.FadeInFromZero(300, Easing.OutQuint)
.ScaleTo(Vector2.One)
.ScaleTo(new Vector2(1.2f), 1800, Easing.OutQuint);
}
this.FadeOutFromOne(800);
@@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
case GameplaySkinComponentLookup<HitResult> resultComponent:
// This should eventually be moved to a skin setting, when supported.
if (Skin is ArgonProSkin && resultComponent.Component >= HitResult.Great)
if (Skin is ArgonProSkin && (resultComponent.Component == HitResult.Great || resultComponent.Component == HitResult.Perfect))
return Drawable.Empty();
return new ArgonJudgementPiece(resultComponent.Component);
@@ -160,7 +160,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
decimal? legacyVersion = skin.GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version)?.Value;
if (legacyVersion >= 2.0m)
if (legacyVersion > 1.0m)
// legacy skins of version 2.0 and newer only apply very short fade out to the number piece.
hitCircleText.FadeOut(legacy_fade_duration / 4);
else
+5 -5
View File
@@ -70,10 +70,10 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
};
userCursorScale = config.GetBindable<float>(OsuSetting.GameplayCursorSize);
userCursorScale.ValueChanged += _ => calculateCursorScale();
userCursorScale.ValueChanged += _ => cursorScale.Value = CalculateCursorScale();
autoCursorScale = config.GetBindable<bool>(OsuSetting.AutoCursorSize);
autoCursorScale.ValueChanged += _ => calculateCursorScale();
autoCursorScale.ValueChanged += _ => cursorScale.Value = CalculateCursorScale();
cursorScale.BindValueChanged(e => cursorScaleContainer.Scale = new Vector2(e.NewValue), true);
}
@@ -81,10 +81,10 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
protected override void LoadComplete()
{
base.LoadComplete();
calculateCursorScale();
cursorScale.Value = CalculateCursorScale();
}
private void calculateCursorScale()
protected virtual float CalculateCursorScale()
{
float scale = userCursorScale.Value;
@@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
scale *= GetScaleForCircleSize(state.Beatmap.Difficulty.CircleSize);
}
cursorScale.Value = scale;
return scale;
}
protected override void SkinChanged(ISkinSource skin)
+19 -4
View File
@@ -20,7 +20,6 @@ using osu.Game.Rulesets.Osu.Configuration;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables.Connections;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
@@ -66,8 +65,21 @@ namespace osu.Game.Rulesets.Osu.UI
HitPolicy = new StartTimeOrderedHitPolicy();
var hitWindows = new OsuHitWindows();
foreach (var result in Enum.GetValues<HitResult>().Where(r => r > HitResult.None && hitWindows.IsHitResultAllowed(r)))
foreach (var result in Enum.GetValues<HitResult>().Where(r =>
{
switch (r)
{
case HitResult.Great:
case HitResult.Ok:
case HitResult.Meh:
case HitResult.Miss:
case HitResult.LargeTickMiss:
case HitResult.IgnoreMiss:
return true;
}
return false;
}))
poolDictionary.Add(result, new DrawableJudgementPool(result, onJudgementLoaded));
AddRangeInternal(poolDictionary.Values);
@@ -170,7 +182,10 @@ namespace osu.Game.Rulesets.Osu.UI
if (!judgedObject.DisplayResult || !DisplayJudgements.Value)
return;
DrawableOsuJudgement explosion = poolDictionary[result.Type].Get(doj => doj.Apply(result, judgedObject));
if (!poolDictionary.TryGetValue(result.Type, out var pool))
return;
DrawableOsuJudgement explosion = pool.Get(doj => doj.Apply(result, judgedObject));
judgementLayer.Add(explosion);
@@ -71,6 +71,12 @@ namespace osu.Game.Rulesets.Osu.UI
RelativePositionAxes = Axes.Both;
}
protected override float CalculateCursorScale()
{
// Force minimum cursor size so it's easily clickable
return Math.Max(1f, base.CalculateCursorScale());
}
protected override bool OnHover(HoverEvent e)
{
updateColour();
@@ -10,7 +10,7 @@ using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Taiko.Tests.Mods
{
public partial class TestSceneTaikoModPerfect : ModPerfectTestScene
public partial class TestSceneTaikoModPerfect : ModFailConditionTestScene
{
protected override Ruleset CreatePlayerRuleset() => new TestTaikoRuleset();
@@ -0,0 +1,41 @@
// 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 NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Judgements;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Scoring;
namespace osu.Game.Rulesets.Taiko.Tests
{
[TestFixture]
public class TaikoScoreProcessorTest
{
[Test]
public void TestInaccurateHitScore()
{
var beatmap = new Beatmap<HitObject>
{
HitObjects =
{
new Hit(),
new Hit { StartTime = 1000 }
}
};
var scoreProcessor = new TaikoScoreProcessor();
scoreProcessor.ApplyBeatmap(beatmap);
// Apply a miss judgement
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new TaikoJudgement()) { Type = HitResult.Great });
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[1], new TaikoJudgement()) { Type = HitResult.Ok });
Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(453745));
Assert.That(scoreProcessor.Accuracy.Value, Is.EqualTo(0.75).Within(0.0001));
}
}
}
@@ -5,7 +5,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
@@ -13,11 +12,14 @@ using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Scoring.Legacy;
using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Scoring;
namespace osu.Game.Rulesets.Taiko.Difficulty
{
internal class TaikoLegacyScoreSimulator : ILegacyScoreSimulator
{
private readonly ScoreProcessor scoreProcessor = new TaikoScoreProcessor();
private int legacyBonusScore;
private int standardisedBonusScore;
private int combo;
@@ -191,7 +193,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
if (isBonus)
{
legacyBonusScore += scoreIncrease;
standardisedBonusScore += Judgement.ToNumericResult(bonusResult);
standardisedBonusScore += scoreProcessor.GetBaseScoreForResult(bonusResult);
}
else
attributes.AccuracyScore += scoreIncrease;
@@ -6,6 +6,7 @@
using System;
using osu.Framework.Graphics;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
@@ -25,7 +26,7 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
private readonly IHasDuration spanPlacementObject;
protected override bool IsValidForPlacement => spanPlacementObject.Duration > 0;
protected override bool IsValidForPlacement => Precision.DefinitelyBigger(spanPlacementObject.Duration, 0);
public TaikoSpanPlacementBlueprint(HitObject hitObject)
: base(hitObject)
@@ -28,11 +28,22 @@ namespace osu.Game.Rulesets.Taiko.Scoring
protected override double GetComboScoreChange(JudgementResult result)
{
return Judgement.ToNumericResult(result.Type)
return GetBaseScoreForResult(result.Type)
* Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(400, combo_base))
* strongScaleValue(result);
}
public override int GetBaseScoreForResult(HitResult result)
{
switch (result)
{
case HitResult.Ok:
return 150;
}
return base.GetBaseScoreForResult(result);
}
private double strongScaleValue(JudgementResult result)
{
if (result.HitObject is StrongNestedHitObject strong)
@@ -219,6 +219,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
{
new OsuModDoubleTime { SpeedChange = { Value = 1.1 } }
};
scoreInfo.OnlineID = 123123;
scoreInfo.ClientVersion = "2023.1221.0";
var beatmap = new TestBeatmap(ruleset);
var score = new Score
@@ -237,9 +239,11 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.Multiple(() =>
{
Assert.That(decodedAfterEncode.ScoreInfo.OnlineID, Is.EqualTo(123123));
Assert.That(decodedAfterEncode.ScoreInfo.Statistics, Is.EqualTo(scoreInfo.Statistics));
Assert.That(decodedAfterEncode.ScoreInfo.MaximumStatistics, Is.EqualTo(scoreInfo.MaximumStatistics));
Assert.That(decodedAfterEncode.ScoreInfo.Mods, Is.EqualTo(scoreInfo.Mods));
Assert.That(decodedAfterEncode.ScoreInfo.ClientVersion, Is.EqualTo("2023.1221.0"));
});
}
@@ -127,8 +127,11 @@ namespace osu.Game.Tests.Database
});
}
[TestCase(30000001)]
[TestCase(30000002)]
[TestCase(30000003)]
[TestCase(30000004)]
[TestCase(30000005)]
public void TestScoreUpgradeSuccess(int scoreVersion)
{
ScoreInfo scoreInfo = null!;
@@ -48,7 +48,7 @@ namespace osu.Game.Tests.Gameplay
// Apply a judgement
scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new TestJudgement(HitResult.LargeBonus)) { Type = HitResult.LargeBonus });
Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(Judgement.LARGE_BONUS_SCORE));
Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(scoreProcessor.GetBaseScoreForResult(HitResult.LargeBonus)));
}
[Test]
@@ -196,6 +196,7 @@ namespace osu.Game.Tests.Scores.IO
User = new APIUser { Username = "Test user" },
BeatmapInfo = beatmap.Beatmaps.First(),
Ruleset = new OsuRuleset().RulesetInfo,
ClientVersion = "12345",
Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() },
};
@@ -203,6 +204,7 @@ namespace osu.Game.Tests.Scores.IO
Assert.IsTrue(imported.Mods.Any(m => m is OsuModHardRock));
Assert.IsTrue(imported.Mods.Any(m => m is OsuModDoubleTime));
Assert.That(imported.ClientVersion, Is.EqualTo(toImport.ClientVersion));
}
finally
{
@@ -17,6 +17,7 @@ using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Timing;
using osu.Game.Screens.Edit.Timing.RowAttributes;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Editing
@@ -69,6 +70,48 @@ namespace osu.Game.Tests.Visual.Editing
AddUntilStep("Wait for rows to load", () => Child.ChildrenOfType<EffectRowAttribute>().Any());
}
[Test]
public void TestSelectedRetainedOverUndo()
{
AddStep("Select first timing point", () =>
{
InputManager.MoveMouseTo(Child.ChildrenOfType<TimingRowAttribute>().First());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 2170);
AddUntilStep("Ensure seeked to correct time", () => EditorClock.CurrentTimeAccurate == 2170);
AddStep("Adjust offset", () =>
{
InputManager.MoveMouseTo(timingScreen.ChildrenOfType<TimingAdjustButton>().First().ScreenSpaceDrawQuad.Centre + new Vector2(20, 0));
InputManager.Click(MouseButton.Left);
});
AddUntilStep("wait for offset changed", () =>
{
return timingScreen.SelectedGroup.Value.ControlPoints.Any(c => c is TimingControlPoint) && timingScreen.SelectedGroup.Value.Time > 2170;
});
AddStep("simulate undo", () =>
{
var clone = editorBeatmap.ControlPointInfo.DeepClone();
editorBeatmap.ControlPointInfo.Clear();
foreach (var group in clone.Groups)
{
foreach (var cp in group.ControlPoints)
editorBeatmap.ControlPointInfo.Add(group.Time, cp);
}
});
AddUntilStep("selection retained", () =>
{
return timingScreen.SelectedGroup.Value.ControlPoints.Any(c => c is TimingControlPoint) && timingScreen.SelectedGroup.Value.Time > 2170;
});
}
[Test]
public void TestTrackingCurrentTimeWhileRunning()
{
@@ -12,6 +12,7 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Scoring;
using osu.Game.Screens.Play.PlayerSettings;
using osu.Game.Tests.Resources;
using osu.Game.Tests.Visual.Ranking;
namespace osu.Game.Tests.Visual.Gameplay
@@ -44,7 +45,23 @@ namespace osu.Game.Tests.Visual.Gameplay
{
offsetControl.ReferenceScore.Value = new ScoreInfo
{
HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(0, 2)
HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(0, 2),
BeatmapInfo = Beatmap.Value.BeatmapInfo,
};
});
AddAssert("No calibration button", () => !offsetControl.ChildrenOfType<SettingsButton>().Any());
}
[Test]
public void TestScoreFromDifferentBeatmap()
{
AddStep("Set short reference score", () =>
{
offsetControl.ReferenceScore.Value = new ScoreInfo
{
HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(10),
BeatmapInfo = TestResources.CreateTestBeatmapSetInfo().Beatmaps.First(),
};
});
@@ -59,7 +76,8 @@ namespace osu.Game.Tests.Visual.Gameplay
offsetControl.ReferenceScore.Value = new ScoreInfo
{
HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(10),
Mods = new Mod[] { new OsuModRelax() }
Mods = new Mod[] { new OsuModRelax() },
BeatmapInfo = Beatmap.Value.BeatmapInfo,
};
});
@@ -77,7 +95,8 @@ namespace osu.Game.Tests.Visual.Gameplay
{
offsetControl.ReferenceScore.Value = new ScoreInfo
{
HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(average_error)
HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(average_error),
BeatmapInfo = Beatmap.Value.BeatmapInfo,
};
});
@@ -105,7 +124,8 @@ namespace osu.Game.Tests.Visual.Gameplay
{
offsetControl.ReferenceScore.Value = new ScoreInfo
{
HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(average_error)
HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(average_error),
BeatmapInfo = Beatmap.Value.BeatmapInfo,
};
});
@@ -41,6 +41,9 @@ namespace osu.Game.Tests.Visual.Gameplay
private BeatmapSetInfo? importedSet;
[Resolved]
private OsuGameBase osu { get; set; } = null!;
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
@@ -153,6 +156,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("results displayed", () => Player.GetChildScreen() is ResultsScreen);
AddUntilStep("score in database", () => Realm.Run(r => r.Find<ScoreInfo>(Player.Score.ScoreInfo.ID) != null));
AddUntilStep("score has correct version", () => Realm.Run(r => r.Find<ScoreInfo>(Player.Score.ScoreInfo.ID)!.ClientVersion), () => Is.EqualTo(osu.Version));
}
[Test]
@@ -0,0 +1,30 @@
// 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.Framework.Testing;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Screens.Menu;
namespace osu.Game.Tests.Visual.Menus
{
public partial class TestSceneMainMenu : OsuGameTestScene
{
[Test]
public void TestSystemTitle()
{
AddStep("set system title", () => Game.ChildrenOfType<SystemTitle>().Single().Current.Value = new APISystemTitle
{
Image = @"https://assets.ppy.sh/main-menu/project-loved-2@2x.png",
Url = @"https://osu.ppy.sh/home/news/2023-12-21-project-loved-december-2023",
});
AddStep("set another title", () => Game.ChildrenOfType<SystemTitle>().Single().Current.Value = new APISystemTitle
{
Image = @"https://assets.ppy.sh/main-menu/wf2023-vote@2x.png",
Url = @"https://osu.ppy.sh/community/contests/189",
});
AddStep("unset system title", () => Game.ChildrenOfType<SystemTitle>().Single().Current.Value = null);
}
}
}
@@ -1,19 +1,27 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Screens.Menu;
namespace osu.Game.Tests.Visual.Menus
{
public partial class TestSceneDisclaimer : ScreenTestScene
public partial class TestSceneSupporterDisplay : OsuTestScene
{
[BackgroundDependencyLoader]
private void load()
[Test]
public void TestBasic()
{
AddStep("load disclaimer", () => LoadScreen(new Disclaimer()));
AddStep("create display", () =>
{
Child = new SupporterDisplay
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
};
});
AddStep("toggle support", () =>
{
@@ -81,6 +81,21 @@ namespace osu.Game.Tests.Visual.Online
AddStep("End watching user presence", () => metadataClient.EndWatchingUserPresence());
}
[Test]
public void TestUserWasPlayingBeforeWatchingUserPresence()
{
AddStep("User began playing", () => spectatorClient.SendStartPlay(streamingUser.Id, 0));
AddStep("Begin watching user presence", () => metadataClient.BeginWatchingUserPresence());
AddStep("Add online user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, new UserPresence { Status = UserStatus.Online, Activity = new UserActivity.ChoosingBeatmap() }));
AddUntilStep("Panel loaded", () => currentlyOnline.ChildrenOfType<UserGridPanel>().FirstOrDefault()?.User.Id == 2);
AddAssert("Spectate button enabled", () => currentlyOnline.ChildrenOfType<PurpleRoundedButton>().First().Enabled.Value, () => Is.True);
AddStep("User finished playing", () => spectatorClient.SendEndPlay(streamingUser.Id));
AddAssert("Spectate button disabled", () => currentlyOnline.ChildrenOfType<PurpleRoundedButton>().First().Enabled.Value, () => Is.False);
AddStep("Remove playing user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, null));
AddStep("End watching user presence", () => metadataClient.EndWatchingUserPresence());
}
internal partial class TestUserLookupCache : UserLookupCache
{
private static readonly string[] usernames =
@@ -0,0 +1,63 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Overlays.Settings.Sections.Audio;
using osu.Game.Scoring;
using osu.Game.Tests.Visual.Ranking;
namespace osu.Game.Tests.Visual.Settings
{
public partial class TestSceneAudioOffsetAdjustControl : OsuTestScene
{
[Resolved]
private SessionStatics statics { get; set; } = null!;
[Cached]
private SessionAverageHitErrorTracker tracker = new SessionAverageHitErrorTracker();
private Container content = null!;
protected override Container Content => content;
[BackgroundDependencyLoader]
private void load()
{
base.Content.AddRange(new Drawable[]
{
tracker,
content = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 400,
AutoSizeAxes = Axes.Y
}
});
}
[Test]
public void TestBehaviour()
{
AddStep("create control", () => Child = new AudioOffsetAdjustControl
{
Current = new BindableDouble
{
MinValue = -500,
MaxValue = 500
}
});
AddStep("set new score", () => statics.SetValue(Static.LastLocalUserScore, new ScoreInfo
{
HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(RNG.NextDouble(-100, 100)),
BeatmapInfo = Beatmap.Value.BeatmapInfo,
}));
AddStep("clear history", () => tracker.ClearHistory());
}
}
}
@@ -13,7 +13,6 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Localisation;
using osu.Game.Overlays;
using osu.Game.Overlays.Settings;
using osu.Game.Overlays.Settings.Sections.Input;
using osu.Game.Rulesets.Taiko;
using osuTK.Input;
@@ -152,7 +151,7 @@ namespace osu.Game.Tests.Visual.Settings
AddStep("click first row with two bindings", () =>
{
multiBindingRow = panel.ChildrenOfType<KeyBindingRow>().First(row => row.Defaults.Count() > 1);
InputManager.MoveMouseTo(multiBindingRow);
InputManager.MoveMouseTo(multiBindingRow.ChildrenOfType<OsuSpriteText>().First());
InputManager.Click(MouseButton.Left);
});
@@ -256,7 +255,7 @@ namespace osu.Game.Tests.Visual.Settings
AddStep("click first row with two bindings", () =>
{
multiBindingRow = panel.ChildrenOfType<KeyBindingRow>().First(row => row.Defaults.Count() > 1);
InputManager.MoveMouseTo(multiBindingRow);
InputManager.MoveMouseTo(multiBindingRow.ChildrenOfType<OsuSpriteText>().First());
InputManager.Click(MouseButton.Left);
});
@@ -305,7 +304,6 @@ namespace osu.Game.Tests.Visual.Settings
section.ChildrenOfType<ResetButton>().Single().TriggerClick();
});
AddStep("move mouse to centre", () => InputManager.MoveMouseTo(panel.ScreenSpaceDrawQuad.Centre));
AddUntilStep("wait for collapsed", () => panel.ChildrenOfType<SettingsSidebar>().Single().Expanded.Value, () => Is.False);
scrollToAndStartBinding("Left (rim)");
AddStep("attempt to bind M1 to two keys", () => InputManager.Click(MouseButton.Left));
@@ -325,7 +323,6 @@ namespace osu.Game.Tests.Visual.Settings
section.ChildrenOfType<ResetButton>().Single().TriggerClick();
});
AddStep("move mouse to centre", () => InputManager.MoveMouseTo(panel.ScreenSpaceDrawQuad.Centre));
AddUntilStep("wait for collapsed", () => panel.ChildrenOfType<SettingsSidebar>().Single().Expanded.Value, () => Is.False);
scrollToAndStartBinding("Left (rim)");
AddStep("attempt to bind M1 to two keys", () => InputManager.Click(MouseButton.Left));
@@ -345,7 +342,6 @@ namespace osu.Game.Tests.Visual.Settings
section.ChildrenOfType<ResetButton>().Single().TriggerClick();
});
AddStep("move mouse to centre", () => InputManager.MoveMouseTo(panel.ScreenSpaceDrawQuad.Centre));
AddUntilStep("wait for collapsed", () => panel.ChildrenOfType<SettingsSidebar>().Single().Expanded.Value, () => Is.False);
scrollToAndStartBinding("Left (centre)");
AddStep("clear binding", () =>
{
@@ -377,7 +373,6 @@ namespace osu.Game.Tests.Visual.Settings
section.ChildrenOfType<ResetButton>().Single().TriggerClick();
});
AddStep("move mouse to centre", () => InputManager.MoveMouseTo(panel.ScreenSpaceDrawQuad.Centre));
AddUntilStep("wait for collapsed", () => panel.ChildrenOfType<SettingsSidebar>().Single().Expanded.Value, () => Is.False);
scrollToAndStartBinding("Left (centre)");
AddStep("clear binding", () =>
{
@@ -41,6 +41,7 @@ namespace osu.Game.Tests.Visual.Settings
public void TestBasic()
{
AddStep("do nothing", () => { });
AddToggleStep("toggle visibility", visible => settings.State.Value = visible ? Visibility.Visible : Visibility.Hidden);
}
[Test]
@@ -110,7 +111,7 @@ namespace osu.Game.Tests.Visual.Settings
AddStep("Press back", () => settings
.ChildrenOfType<KeyBindingPanel>().FirstOrDefault()?
.ChildrenOfType<SettingsSubPanel.BackButton>().FirstOrDefault()?.TriggerClick());
.ChildrenOfType<SettingsSidebar.BackButton>().FirstOrDefault()?.TriggerClick());
AddUntilStep("top-level textbox focused", () => settings.SectionsContainer.ChildrenOfType<SettingsSearchTextBox>().FirstOrDefault()?.HasFocus == true);
}
@@ -454,6 +454,23 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("Un-filter", () => carousel.Filter(new FilterCriteria(), false));
}
[Test]
public void TestRewind()
{
const int local_set_count = 3;
const int random_select_count = local_set_count * 3;
loadBeatmaps(setCount: local_set_count);
for (int i = 0; i < random_select_count; i++)
nextRandom();
for (int i = 0; i < random_select_count; i++)
{
prevRandom();
AddAssert("correct random last selected", () => selectedSets.Peek(), () => Is.EqualTo(carousel.SelectedBeatmapSet));
}
}
[Test]
public void TestRewindToDeletedBeatmap()
{
@@ -3,6 +3,7 @@
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
@@ -14,6 +15,7 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Extensions;
using osu.Game.Graphics.Sprites;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets;
@@ -194,6 +196,36 @@ namespace osu.Game.Tests.Visual.SongSelect
});
}
[TestCase]
public void TestLengthUpdates()
{
IBeatmap beatmap = createTestBeatmap(new OsuRuleset().RulesetInfo);
double drain = beatmap.CalculateDrainLength();
beatmap.BeatmapInfo.Length = drain;
OsuModDoubleTime doubleTime = null;
selectBeatmap(beatmap);
checkDisplayedLength(drain);
AddStep("select DT", () => SelectedMods.Value = new[] { doubleTime = new OsuModDoubleTime() });
checkDisplayedLength(Math.Round(drain / 1.5f));
AddStep("change DT rate", () => doubleTime.SpeedChange.Value = 2);
checkDisplayedLength(Math.Round(drain / 2));
}
private void checkDisplayedLength(double drain)
{
var displayedLength = drain.ToFormattedDuration();
AddUntilStep($"check map drain ({displayedLength})", () =>
{
var label = infoWedge.DisplayedContent.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Single(l => l.Statistic.Name == BeatmapsetsStrings.ShowStatsTotalLength(displayedLength));
return label.Statistic.Content == displayedLength.ToString();
});
}
private void setRuleset(RulesetInfo rulesetInfo)
{
Container containerBefore = null;
@@ -420,6 +420,7 @@ namespace osu.Game.Tests.Visual.SongSelect
}
[Test]
[Ignore("temporary while peppy investigates. probably realm batching related.")]
public void TestSelectionRetainedOnBeatmapUpdate()
{
createSongSelect();
@@ -464,7 +465,7 @@ namespace osu.Game.Tests.Visual.SongSelect
manager.Import(testBeatmapSetInfo);
}, 10);
AddUntilStep("has selection", () => songSelect!.Carousel.SelectedBeatmapInfo?.BeatmapSet?.OnlineID == originalOnlineSetID);
AddUntilStep("has selection", () => songSelect!.Carousel.SelectedBeatmapInfo?.BeatmapSet?.OnlineID, () => Is.EqualTo(originalOnlineSetID));
Task<Live<BeatmapSetInfo>?> updateTask = null!;
@@ -476,7 +477,7 @@ namespace osu.Game.Tests.Visual.SongSelect
});
AddUntilStep("wait for update completion", () => updateTask.IsCompleted);
AddUntilStep("retained selection", () => songSelect!.Carousel.SelectedBeatmapInfo?.BeatmapSet?.OnlineID == originalOnlineSetID);
AddUntilStep("retained selection", () => songSelect!.Carousel.SelectedBeatmapInfo?.BeatmapSet?.OnlineID, () => Is.EqualTo(originalOnlineSetID));
}
[Test]
@@ -9,6 +9,7 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Game.Overlays;
using osu.Game.Overlays.Dialog;
@@ -19,11 +20,15 @@ namespace osu.Game.Tests.Visual.UserInterface
{
private DialogOverlay overlay;
[SetUpSteps]
public void SetUpSteps()
{
AddStep("create dialog overlay", () => Child = overlay = new DialogOverlay());
}
[Test]
public void TestBasic()
{
AddStep("create dialog overlay", () => Child = overlay = new DialogOverlay());
TestPopupDialog firstDialog = null;
TestPopupDialog secondDialog = null;
@@ -84,7 +89,31 @@ namespace osu.Game.Tests.Visual.UserInterface
}));
AddAssert("second dialog displayed", () => overlay.CurrentDialog == secondDialog);
AddAssert("first dialog is not part of hierarchy", () => firstDialog.Parent == null);
AddUntilStep("first dialog is not part of hierarchy", () => firstDialog.Parent == null);
}
[Test]
public void TestTooMuchText()
{
AddStep("dialog #1", () => overlay.Push(new TestPopupDialog
{
Icon = FontAwesome.Regular.TrashAlt,
HeaderText = @"Confirm deletion ofConfirm deletion ofConfirm deletion ofConfirm deletion ofConfirm deletion ofConfirm deletion of",
BodyText = @"Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver. ",
Buttons = new PopupDialogButton[]
{
new PopupDialogOkButton
{
Text = @"I never want to see this again.",
Action = () => Console.WriteLine(@"OK"),
},
new PopupDialogCancelButton
{
Text = @"Firetruck, I still want quick ranks!",
Action = () => Console.WriteLine(@"Cancel"),
},
},
}));
}
[Test]
@@ -92,7 +121,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
PopupDialog dialog = null;
AddStep("create dialog overlay", () => overlay = new SlowLoadingDialogOverlay());
AddStep("create slow loading dialog overlay", () => overlay = new SlowLoadingDialogOverlay());
AddStep("start loading overlay", () => LoadComponentAsync(overlay, Add));
@@ -128,8 +157,6 @@ namespace osu.Game.Tests.Visual.UserInterface
[Test]
public void TestDismissBeforePush()
{
AddStep("create dialog overlay", () => Child = overlay = new DialogOverlay());
TestPopupDialog testDialog = null;
AddStep("dismissed dialog push", () =>
{
@@ -146,8 +173,6 @@ namespace osu.Game.Tests.Visual.UserInterface
[Test]
public void TestDismissBeforePushViaButtonPress()
{
AddStep("create dialog overlay", () => Child = overlay = new DialogOverlay());
TestPopupDialog testDialog = null;
AddStep("dismissed dialog push", () =>
{
@@ -163,7 +188,7 @@ namespace osu.Game.Tests.Visual.UserInterface
});
AddAssert("no dialog pushed", () => overlay.CurrentDialog == null);
AddAssert("dialog is not part of hierarchy", () => testDialog.Parent == null);
AddUntilStep("dialog is not part of hierarchy", () => testDialog.Parent == null);
}
private partial class TestPopupDialog : PopupDialog
@@ -572,7 +572,7 @@ namespace osu.Game.Tests.Visual.UserInterface
[Test]
public void TestTextSearchActiveByDefault()
{
configManager.SetValue(OsuSetting.ModSelectTextSearchStartsActive, true);
AddStep("text search starts active", () => configManager.SetValue(OsuSetting.ModSelectTextSearchStartsActive, true));
createScreen();
AddUntilStep("search text box focused", () => modSelectOverlay.SearchTextBox.HasFocus);
@@ -587,7 +587,7 @@ namespace osu.Game.Tests.Visual.UserInterface
[Test]
public void TestTextSearchNotActiveByDefault()
{
configManager.SetValue(OsuSetting.ModSelectTextSearchStartsActive, false);
AddStep("text search does not start active", () => configManager.SetValue(OsuSetting.ModSelectTextSearchStartsActive, false));
createScreen();
AddUntilStep("search text box not focused", () => !modSelectOverlay.SearchTextBox.HasFocus);
@@ -599,6 +599,31 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("search text box unfocused", () => !modSelectOverlay.SearchTextBox.HasFocus);
}
[Test]
public void TestTextSearchDoesNotBlockCustomisationPanelKeyboardInteractions()
{
AddStep("text search starts active", () => configManager.SetValue(OsuSetting.ModSelectTextSearchStartsActive, true));
createScreen();
AddUntilStep("search text box focused", () => modSelectOverlay.SearchTextBox.HasFocus);
AddStep("select DT", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime() });
AddAssert("DT selected", () => modSelectOverlay.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value), () => Is.EqualTo(1));
AddStep("open customisation area", () => modSelectOverlay.CustomisationButton!.TriggerClick());
assertCustomisationToggleState(false, true);
AddStep("hover over mod settings slider", () =>
{
var slider = modSelectOverlay.ChildrenOfType<ModSettingsArea>().Single().ChildrenOfType<OsuSliderBar<double>>().First();
InputManager.MoveMouseTo(slider);
});
AddStep("press right arrow", () => InputManager.PressKey(Key.Right));
AddAssert("DT speed changed", () => !SelectedMods.Value.OfType<OsuModDoubleTime>().Single().SpeedChange.IsDefault);
AddStep("close customisation area", () => InputManager.PressKey(Key.Escape));
AddUntilStep("search text box reacquired focus", () => modSelectOverlay.SearchTextBox.HasFocus);
}
[Test]
public void TestDeselectAllViaKey()
{
@@ -154,7 +154,7 @@ namespace osu.Game.Tests.Visual.UserInterface
public TestTitle()
{
Title = "title";
Icon = HexaconsIcons.Devtools;
Icon = OsuIcon.ChangelogB;
}
}
}
@@ -1,10 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Game.Overlays.Dialog;
@@ -15,24 +12,25 @@ namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestScenePopupDialog : OsuManualInputManagerTestScene
{
private TestPopupDialog dialog;
private TestPopupDialog dialog = null!;
[SetUpSteps]
public void SetUpSteps()
{
AddStep("new popup", () =>
{
Add(dialog = new TestPopupDialog
Child = dialog = new TestPopupDialog
{
RelativeSizeAxes = Axes.Both,
State = { Value = Framework.Graphics.Containers.Visibility.Visible },
});
};
});
}
[Test]
public void TestDangerousButton([Values(false, true)] bool atEdge)
{
AddStep("finish transforms", () => dialog.FinishTransforms(true));
if (atEdge)
{
AddStep("move mouse to button edge", () =>
+7 -3
View File
@@ -96,10 +96,14 @@ namespace osu.Game.Audio
hasStarted = false;
Track.Stop();
// This pre-check is important, fixes a BASS deadlock in some scenarios.
if (!Track.HasCompleted)
{
Track.Stop();
// Ensure the track is reset immediately on stopping, so the next time it is started it has a correct time value.
Track.Seek(0);
// Ensure the track is reset immediately on stopping, so the next time it is started it has a correct time value.
Track.Seek(0);
}
Stopped?.Invoke();
}
+103 -25
View File
@@ -67,6 +67,7 @@ namespace osu.Game
checkForOutdatedStarRatings();
processBeatmapSetsWithMissingMetrics();
// Note that the previous method will also update these on a fresh run.
processBeatmapsWithMissingObjectCounts();
processScoresWithMissingStatistics();
convertLegacyTotalScoreToStandardised();
@@ -144,12 +145,24 @@ namespace osu.Game
}
});
if (beatmapSetIds.Count == 0)
return;
Logger.Log($"Found {beatmapSetIds.Count} beatmap sets which require reprocessing.");
int i = 0;
// Technically this is doing more than just star ratings, but easier for the end user to understand.
var notification = showProgressNotification(beatmapSetIds.Count, "Reprocessing star rating for beatmaps", "beatmaps' star ratings have been updated");
int processedCount = 0;
int failedCount = 0;
foreach (var id in beatmapSetIds)
{
if (notification?.State == ProgressNotificationState.Cancelled)
break;
updateNotificationProgress(notification, processedCount, beatmapSetIds.Count);
sleepIfRequired();
realmAccess.Run(r =>
@@ -160,16 +173,19 @@ namespace osu.Game
{
try
{
Logger.Log($"Background processing {set} ({++i} / {beatmapSetIds.Count})");
beatmapUpdater.Process(set);
++processedCount;
}
catch (Exception e)
{
Logger.Log($"Background processing failed on {set}: {e}");
++failedCount;
}
}
});
}
completeNotification(notification, processedCount, beatmapSetIds.Count, failedCount);
}
private void processBeatmapsWithMissingObjectCounts()
@@ -180,16 +196,27 @@ namespace osu.Game
realmAccess.Run(r =>
{
foreach (var b in r.All<BeatmapInfo>().Where(b => b.TotalObjectCount == 0))
foreach (var b in r.All<BeatmapInfo>().Where(b => b.TotalObjectCount < 0 || b.EndTimeObjectCount < 0))
beatmapIds.Add(b.ID);
});
Logger.Log($"Found {beatmapIds.Count} beatmaps which require reprocessing.");
if (beatmapIds.Count == 0)
return;
int i = 0;
Logger.Log($"Found {beatmapIds.Count} beatmaps which require statistics population.");
var notification = showProgressNotification(beatmapIds.Count, "Populating missing statistics for beatmaps", "beatmaps have been populated with missing statistics");
int processedCount = 0;
int failedCount = 0;
foreach (var id in beatmapIds)
{
if (notification?.State == ProgressNotificationState.Cancelled)
break;
updateNotificationProgress(notification, processedCount, beatmapIds.Count);
sleepIfRequired();
realmAccess.Run(r =>
@@ -200,16 +227,19 @@ namespace osu.Game
{
try
{
Logger.Log($"Background processing {beatmap} ({++i} / {beatmapIds.Count})");
beatmapUpdater.ProcessObjectCounts(beatmap);
++processedCount;
}
catch (Exception e)
{
Logger.Log($"Background processing failed on {beatmap}: {e}");
++failedCount;
}
}
});
}
completeNotification(notification, processedCount, beatmapIds.Count, failedCount);
}
private void processScoresWithMissingStatistics()
@@ -231,10 +261,23 @@ namespace osu.Game
}
});
Logger.Log($"Found {scoreIds.Count} scores which require reprocessing.");
if (scoreIds.Count == 0)
return;
Logger.Log($"Found {scoreIds.Count} scores which require statistics population.");
var notification = showProgressNotification(scoreIds.Count, "Populating missing statistics for scores", "scores have been populated with missing statistics");
int processedCount = 0;
int failedCount = 0;
foreach (var id in scoreIds)
{
if (notification?.State == ProgressNotificationState.Cancelled)
break;
updateNotificationProgress(notification, processedCount, scoreIds.Count);
sleepIfRequired();
try
@@ -250,7 +293,7 @@ namespace osu.Game
r.Find<ScoreInfo>(id)!.MaximumStatisticsJson = JsonConvert.SerializeObject(score.MaximumStatistics);
});
Logger.Log($"Populated maximum statistics for score {id}");
++processedCount;
}
catch (ObjectDisposedException)
{
@@ -260,8 +303,11 @@ namespace osu.Game
{
Logger.Log(@$"Failed to populate maximum statistics for {id}: {e}");
realmAccess.Write(r => r.Find<ScoreInfo>(id)!.BackgroundReprocessingFailed = true);
++failedCount;
}
}
completeNotification(notification, processedCount, scoreIds.Count, failedCount);
}
private void convertLegacyTotalScoreToStandardised()
@@ -270,8 +316,7 @@ namespace osu.Game
HashSet<Guid> scoreIds = realmAccess.Run(r => new HashSet<Guid>(r.All<ScoreInfo>()
.Where(s => !s.BackgroundReprocessingFailed && s.BeatmapInfo != null
&& (s.TotalScoreVersion == 30000002
|| s.TotalScoreVersion == 30000003))
&& s.TotalScoreVersion < LegacyScoreEncoder.LATEST_VERSION)
.AsEnumerable().Select(s => s.ID)));
Logger.Log($"Found {scoreIds.Count} scores which require total score conversion.");
@@ -279,38 +324,31 @@ namespace osu.Game
if (scoreIds.Count == 0)
return;
ProgressNotification notification = new ProgressNotification { State = ProgressNotificationState.Active };
notificationOverlay?.Post(notification);
var notification = showProgressNotification(scoreIds.Count, "Upgrading scores to new scoring algorithm", "scores have been upgraded to the new scoring algorithm");
int processedCount = 0;
int failedCount = 0;
foreach (var id in scoreIds)
{
if (notification.State == ProgressNotificationState.Cancelled)
if (notification?.State == ProgressNotificationState.Cancelled)
break;
notification.Text = $"Upgrading scores to new scoring algorithm ({processedCount} of {scoreIds.Count})";
notification.Progress = (float)processedCount / scoreIds.Count;
updateNotificationProgress(notification, processedCount, scoreIds.Count);
sleepIfRequired();
try
{
var score = scoreManager.Query(s => s.ID == id);
long newTotalScore = StandardisedScoreMigrationTools.ConvertFromLegacyTotalScore(score, beatmapManager);
// Can't use async overload because we're not on the update thread.
// ReSharper disable once MethodHasAsyncOverload
realmAccess.Write(r =>
{
ScoreInfo s = r.Find<ScoreInfo>(id)!;
s.TotalScore = newTotalScore;
StandardisedScoreMigrationTools.UpdateFromLegacy(s, beatmapManager);
s.TotalScoreVersion = LegacyScoreEncoder.LATEST_VERSION;
});
Logger.Log($"Converted total score for score {id}");
++processedCount;
}
catch (ObjectDisposedException)
@@ -325,24 +363,64 @@ namespace osu.Game
}
}
if (processedCount == scoreIds.Count)
completeNotification(notification, processedCount, scoreIds.Count, failedCount);
}
private void updateNotificationProgress(ProgressNotification? notification, int processedCount, int totalCount)
{
if (notification == null)
return;
notification.Text = notification.Text.ToString().Split('(').First().TrimEnd() + $" ({processedCount} of {totalCount})";
notification.Progress = (float)processedCount / totalCount;
if (processedCount % 100 == 0)
Logger.Log(notification.Text.ToString());
}
private void completeNotification(ProgressNotification? notification, int processedCount, int totalCount, int? failedCount = null)
{
if (notification == null)
return;
if (processedCount == totalCount)
{
notification.CompletionText = $"{processedCount} score(s) have been upgraded to the new scoring algorithm";
notification.CompletionText = $"{processedCount} {notification.CompletionText}";
notification.Progress = 1;
notification.State = ProgressNotificationState.Completed;
}
else
{
notification.Text = $"{processedCount} of {scoreIds.Count} score(s) have been upgraded to the new scoring algorithm.";
notification.Text = $"{processedCount} of {totalCount} {notification.CompletionText}";
// We may have arrived here due to user cancellation or completion with failures.
if (failedCount > 0)
notification.Text += $" Check logs for issues with {failedCount} failed upgrades.";
notification.Text += $" Check logs for issues with {failedCount} failed items.";
notification.State = ProgressNotificationState.Cancelled;
}
}
private ProgressNotification? showProgressNotification(int totalCount, string running, string completed)
{
if (notificationOverlay == null)
return null;
if (totalCount < 10)
return null;
ProgressNotification notification = new ProgressNotification
{
Text = running,
CompletionText = completed,
State = ProgressNotificationState.Active
};
notificationOverlay?.Post(notification);
return notification;
}
private void sleepIfRequired()
{
while (localUserPlayInfo?.IsPlaying.Value == true)
+2 -2
View File
@@ -120,9 +120,9 @@ namespace osu.Game.Beatmaps
[JsonIgnore]
public bool Hidden { get; set; }
public int EndTimeObjectCount { get; set; }
public int EndTimeObjectCount { get; set; } = -1;
public int TotalObjectCount { get; set; }
public int TotalObjectCount { get; set; } = -1;
/// <summary>
/// Reset any fetched online linking information (and history).
+2
View File
@@ -77,6 +77,8 @@ namespace osu.Game.Beatmaps
beatmap.StarRating = calculator.Calculate().StarRating;
beatmap.Length = working.Beatmap.CalculatePlayableLength();
beatmap.BPM = 60000 / working.Beatmap.GetMostCommonBeatLength();
beatmap.EndTimeObjectCount = working.Beatmap.HitObjects.Count(h => h is IHasDuration);
beatmap.TotalObjectCount = working.Beatmap.HitObjects.Count;
}
// And invalidate again afterwards as re-fetching the most up-to-date database metadata will be required.
+3
View File
@@ -59,11 +59,13 @@ namespace osu.Game.Beatmaps
/// <summary>
/// The basic star rating for this beatmap (with no mods applied).
/// Defaults to -1 (meaning not-yet-calculated).
/// </summary>
double StarRating { get; }
/// <summary>
/// The number of hitobjects in the beatmap with a distinct end time.
/// Defaults to -1 (meaning not-yet-calculated).
/// </summary>
/// <remarks>
/// Canonically, these are hitobjects are either sliders or spinners.
@@ -72,6 +74,7 @@ namespace osu.Game.Beatmaps
/// <summary>
/// The total number of hitobjects in the beatmap.
/// Defaults to -1 (meaning not-yet-calculated).
/// </summary>
int TotalObjectCount { get; }
}
+3 -1
View File
@@ -37,7 +37,7 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.Ruleset, string.Empty);
SetDefault(OsuSetting.Skin, SkinInfo.ARGON_SKIN.ToString());
SetDefault(OsuSetting.BeatmapDetailTab, PlayBeatmapDetailArea.TabType.Details);
SetDefault(OsuSetting.BeatmapDetailTab, PlayBeatmapDetailArea.TabType.Local);
SetDefault(OsuSetting.BeatmapDetailModsFilter, false);
SetDefault(OsuSetting.ShowConvertedBeatmaps, true);
@@ -96,6 +96,7 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.MenuVoice, true);
SetDefault(OsuSetting.MenuMusic, true);
SetDefault(OsuSetting.MenuTips, true);
SetDefault(OsuSetting.AudioOffset, 0, -500.0, 500.0, 1);
@@ -350,6 +351,7 @@ namespace osu.Game.Configuration
VolumeInactive,
MenuMusic,
MenuVoice,
MenuTips,
CursorRotation,
MenuParallax,
Prefer24HourTime,
@@ -0,0 +1,54 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
namespace osu.Game.Configuration
{
/// <summary>
/// Tracks the local user's average hit error during the ongoing play session.
/// </summary>
[Cached]
public partial class SessionAverageHitErrorTracker : Component
{
public IBindableList<double> AverageHitErrorHistory => averageHitErrorHistory;
private readonly BindableList<double> averageHitErrorHistory = new BindableList<double>();
private readonly Bindable<ScoreInfo?> latestScore = new Bindable<ScoreInfo?>();
[BackgroundDependencyLoader]
private void load(SessionStatics statics)
{
statics.BindWith(Static.LastLocalUserScore, latestScore);
latestScore.BindValueChanged(score => calculateAverageHitError(score.NewValue), true);
}
private void calculateAverageHitError(ScoreInfo? newScore)
{
if (newScore == null)
return;
if (newScore.Mods.Any(m => !m.UserPlayable || m is IHasNoTimedInputs))
return;
if (newScore.HitEvents.Count < 10)
return;
if (newScore.HitEvents.CalculateAverageHitError() is not double averageError)
return;
// keep a sane maximum number of entries.
if (averageHitErrorHistory.Count >= 50)
averageHitErrorHistory.RemoveAt(0);
averageHitErrorHistory.Add(averageError);
}
public void ClearHistory() => averageHitErrorHistory.Clear();
}
}
+7
View File
@@ -9,6 +9,7 @@ using osu.Game.Input;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Scoring;
namespace osu.Game.Configuration
{
@@ -27,6 +28,7 @@ namespace osu.Game.Configuration
SetDefault(Static.LastModSelectPanelSamplePlaybackTime, (double?)null);
SetDefault<APISeasonalBackgrounds>(Static.SeasonalBackgrounds, null);
SetDefault(Static.TouchInputActive, RuntimeInfo.IsMobile);
SetDefault<ScoreInfo>(Static.LastLocalUserScore, null);
}
/// <summary>
@@ -73,5 +75,10 @@ namespace osu.Game.Configuration
/// Used in touchscreen detection scenarios (<see cref="TouchInputInterceptor"/>).
/// </summary>
TouchInputActive,
/// <summary>
/// Stores the local user's last score (can be completed or aborted).
/// </summary>
LastLocalUserScore,
}
}
+17 -1
View File
@@ -89,8 +89,10 @@ namespace osu.Game.Database
/// 35 2023-10-16 Clear key combinations of keybindings that are assigned to more than one action in a given settings section.
/// 36 2023-10-26 Add LegacyOnlineID to ScoreInfo. Move osu_scores_*_high IDs stored in OnlineID to LegacyOnlineID. Reset anomalous OnlineIDs.
/// 38 2023-12-10 Add EndTimeObjectCount and TotalObjectCount to BeatmapInfo.
/// 39 2023-12-19 Migrate any EndTimeObjectCount and TotalObjectCount values of 0 to -1 to better identify non-calculated values.
/// 40 2023-12-21 Add ScoreInfo.Version to keep track of which build scores were set on.
/// </summary>
private const int schema_version = 38;
private const int schema_version = 40;
/// <summary>
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
@@ -1095,6 +1097,20 @@ namespace osu.Game.Database
break;
}
case 39:
foreach (var b in migration.NewRealm.All<BeatmapInfo>())
{
// Either actually no objects, or processing ran and failed.
// Reset to -1 so the next time they become zero we know that processing was attempted.
if (b.TotalObjectCount == 0 && b.EndTimeObjectCount == 0)
{
b.TotalObjectCount = -1;
b.EndTimeObjectCount = -1;
}
}
break;
}
Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms");
@@ -26,7 +26,7 @@ namespace osu.Game.Database
if (score.IsLegacyScore)
return false;
if (score.TotalScoreVersion > 30000004)
if (score.TotalScoreVersion > 30000002)
return false;
// Recalculate the old-style standardised score to see if this was an old lazer score.
@@ -57,14 +57,14 @@ namespace osu.Game.Database
// We are constructing a "best possible" score from the statistics provided because it's the best we can do.
List<HitResult> sortedHits = score.Statistics
.Where(kvp => kvp.Key.AffectsCombo())
.OrderByDescending(kvp => Judgement.ToNumericResult(kvp.Key))
.OrderByDescending(kvp => processor.GetBaseScoreForResult(kvp.Key))
.SelectMany(kvp => Enumerable.Repeat(kvp.Key, kvp.Value))
.ToList();
// Attempt to use maximum statistics from the database.
var maximumJudgements = score.MaximumStatistics
.Where(kvp => kvp.Key.AffectsCombo())
.OrderByDescending(kvp => Judgement.ToNumericResult(kvp.Key))
.OrderByDescending(kvp => processor.GetBaseScoreForResult(kvp.Key))
.SelectMany(kvp => Enumerable.Repeat(new FakeJudgement(kvp.Key), kvp.Value))
.ToList();
@@ -169,10 +169,10 @@ namespace osu.Game.Database
public static long GetOldStandardised(ScoreInfo score)
{
double accuracyScore =
(double)score.Statistics.Where(kvp => kvp.Key.AffectsAccuracy()).Sum(kvp => Judgement.ToNumericResult(kvp.Key) * kvp.Value)
/ score.MaximumStatistics.Where(kvp => kvp.Key.AffectsAccuracy()).Sum(kvp => Judgement.ToNumericResult(kvp.Key) * kvp.Value);
(double)score.Statistics.Where(kvp => kvp.Key.AffectsAccuracy()).Sum(kvp => numericScoreFor(kvp.Key) * kvp.Value)
/ score.MaximumStatistics.Where(kvp => kvp.Key.AffectsAccuracy()).Sum(kvp => numericScoreFor(kvp.Key) * kvp.Value);
double comboScore = (double)score.MaxCombo / score.MaximumStatistics.Where(kvp => kvp.Key.AffectsCombo()).Sum(kvp => kvp.Value);
double bonusScore = score.Statistics.Where(kvp => kvp.Key.IsBonus()).Sum(kvp => Judgement.ToNumericResult(kvp.Key) * kvp.Value);
double bonusScore = score.Statistics.Where(kvp => kvp.Key.IsBonus()).Sum(kvp => numericScoreFor(kvp.Key) * kvp.Value);
double accuracyPortion = 0.3;
@@ -193,6 +193,65 @@ namespace osu.Game.Database
modMultiplier *= mod.ScoreMultiplier;
return (long)Math.Round((1000000 * (accuracyPortion * accuracyScore + (1 - accuracyPortion) * comboScore) + bonusScore) * modMultiplier);
static int numericScoreFor(HitResult result)
{
switch (result)
{
default:
return 0;
case HitResult.SmallTickHit:
return 10;
case HitResult.LargeTickHit:
return 30;
case HitResult.Meh:
return 50;
case HitResult.Ok:
return 100;
case HitResult.Good:
return 200;
case HitResult.Great:
return 300;
case HitResult.Perfect:
return 315;
case HitResult.SmallBonus:
return 10;
case HitResult.LargeBonus:
return 50;
}
}
}
/// <summary>
/// Updates a legacy <see cref="ScoreInfo"/> to standardised scoring.
/// </summary>
/// <param name="score">The score to update.</param>
/// <param name="beatmaps">A <see cref="BeatmapManager"/> used for <see cref="WorkingBeatmap"/> lookups.</param>
public static void UpdateFromLegacy(ScoreInfo score, BeatmapManager beatmaps)
{
score.TotalScore = convertFromLegacyTotalScore(score, beatmaps);
score.Accuracy = ComputeAccuracy(score);
}
/// <summary>
/// Updates a legacy <see cref="ScoreInfo"/> to standardised scoring.
/// </summary>
/// <param name="score">The score to update.</param>
/// <param name="difficulty">The beatmap difficulty.</param>
/// <param name="attributes">The legacy scoring attributes for the beatmap which the score was set on.</param>
public static void UpdateFromLegacy(ScoreInfo score, LegacyBeatmapConversionDifficultyInfo difficulty, LegacyScoreAttributes attributes)
{
score.TotalScore = convertFromLegacyTotalScore(score, difficulty, attributes);
score.Accuracy = ComputeAccuracy(score);
}
/// <summary>
@@ -201,7 +260,7 @@ namespace osu.Game.Database
/// <param name="score">The score to convert the total score of.</param>
/// <param name="beatmaps">A <see cref="BeatmapManager"/> used for <see cref="WorkingBeatmap"/> lookups.</param>
/// <returns>The standardised total score.</returns>
public static long ConvertFromLegacyTotalScore(ScoreInfo score, BeatmapManager beatmaps)
private static long convertFromLegacyTotalScore(ScoreInfo score, BeatmapManager beatmaps)
{
if (!score.IsLegacyScore)
return score.TotalScore;
@@ -224,7 +283,7 @@ namespace osu.Game.Database
ILegacyScoreSimulator sv1Simulator = legacyRuleset.CreateLegacyScoreSimulator();
LegacyScoreAttributes attributes = sv1Simulator.Simulate(beatmap, playableBeatmap);
return ConvertFromLegacyTotalScore(score, LegacyBeatmapConversionDifficultyInfo.FromBeatmap(beatmap.Beatmap), attributes);
return convertFromLegacyTotalScore(score, LegacyBeatmapConversionDifficultyInfo.FromBeatmap(beatmap.Beatmap), attributes);
}
/// <summary>
@@ -234,7 +293,7 @@ namespace osu.Game.Database
/// <param name="difficulty">The beatmap difficulty.</param>
/// <param name="attributes">The legacy scoring attributes for the beatmap which the score was set on.</param>
/// <returns>The standardised total score.</returns>
public static long ConvertFromLegacyTotalScore(ScoreInfo score, LegacyBeatmapConversionDifficultyInfo difficulty, LegacyScoreAttributes attributes)
private static long convertFromLegacyTotalScore(ScoreInfo score, LegacyBeatmapConversionDifficultyInfo difficulty, LegacyScoreAttributes attributes)
{
if (!score.IsLegacyScore)
return score.TotalScore;
@@ -253,8 +312,12 @@ namespace osu.Game.Database
double legacyAccScore = maximumLegacyAccuracyScore * score.Accuracy;
// We can not separate the ComboScore from the BonusScore, so we keep the bonus in the ratio.
double comboProportion =
((double)score.LegacyTotalScore - legacyAccScore) / (maximumLegacyComboScore + maximumLegacyBonusScore);
// Note that `maximumLegacyComboScore + maximumLegacyBonusScore` can actually be 0
// when playing a beatmap with no bonus objects, with mods that have a 0.0x multiplier on stable (relax/autopilot).
// In such cases, just assume 0.
double comboProportion = maximumLegacyComboScore + maximumLegacyBonusScore > 0
? ((double)score.LegacyTotalScore - legacyAccScore) / (maximumLegacyComboScore + maximumLegacyBonusScore)
: 0;
// We assume the bonus proportion only makes up the rest of the score that exceeds maximumLegacyBaseScore.
long maximumLegacyBaseScore = maximumLegacyAccuracyScore + maximumLegacyComboScore;
@@ -262,6 +325,8 @@ namespace osu.Game.Database
double modMultiplier = score.Mods.Select(m => m.ScoreMultiplier).Aggregate(1.0, (c, n) => c * n);
long convertedTotalScore;
switch (score.Ruleset.OnlineID)
{
case 0:
@@ -293,13 +358,23 @@ namespace osu.Game.Database
// Roughly corresponds to integrating f(combo) = combo ^ COMBO_EXPONENT (omitting constants)
double maximumAchievableComboPortionInStandardisedScore = Math.Pow(maximumLegacyCombo, 1 + ScoreProcessor.COMBO_EXPONENT);
double comboPortionInScoreV1 = maximumAchievableComboPortionInScoreV1 * comboProportion / score.Accuracy;
// This is - roughly - how much score, in the combo portion, the longest combo on this particular play would gain in score V1.
double comboPortionFromLongestComboInScoreV1 = Math.Pow(score.MaxCombo, 2);
// Same for standardised score.
double comboPortionFromLongestComboInStandardisedScore = Math.Pow(score.MaxCombo, 1 + ScoreProcessor.COMBO_EXPONENT);
// We estimate the combo portion of the score in score V1 terms.
// The division by accuracy is supposed to lessen the impact of accuracy on the combo portion,
// but in some edge cases it cannot sanely undo it.
// Therefore the resultant value is clamped from both sides for sanity.
// The clamp from below to `comboPortionFromLongestComboInScoreV1` targets near-FC scores wherein
// the player had bad accuracy at the end of their longest combo, which causes the division by accuracy
// to underestimate the combo portion.
// Ideally, this would be clamped from above to `maximumAchievableComboPortionInScoreV1` too,
// but in practice this appears to fail for some scores (https://github.com/ppy/osu/pull/25876#issuecomment-1862248413).
// TODO: investigate the above more closely
double comboPortionInScoreV1 = Math.Max(maximumAchievableComboPortionInScoreV1 * comboProportion / score.Accuracy, comboPortionFromLongestComboInScoreV1);
// Calculate how many times the longest combo the user has achieved in the play can repeat
// without exceeding the combo portion in score V1 as achieved by the player.
// This is a pessimistic estimate; it intentionally does not operate on object count and uses only score instead.
@@ -348,32 +423,55 @@ namespace osu.Game.Database
double newComboScoreProportion = estimatedComboPortionInStandardisedScore / maximumAchievableComboPortionInStandardisedScore;
return (long)Math.Round((
convertedTotalScore = (long)Math.Round((
500000 * newComboScoreProportion * score.Accuracy
+ 500000 * Math.Pow(score.Accuracy, 5)
+ bonusProportion) * modMultiplier);
break;
case 1:
return (long)Math.Round((
convertedTotalScore = (long)Math.Round((
250000 * comboProportion
+ 750000 * Math.Pow(score.Accuracy, 3.6)
+ bonusProportion) * modMultiplier);
break;
case 2:
return (long)Math.Round((
convertedTotalScore = (long)Math.Round((
600000 * comboProportion
+ 400000 * score.Accuracy
+ bonusProportion) * modMultiplier);
break;
case 3:
return (long)Math.Round((
990000 * comboProportion
+ 10000 * Math.Pow(score.Accuracy, 2 + 2 * score.Accuracy)
convertedTotalScore = (long)Math.Round((
850000 * comboProportion
+ 150000 * Math.Pow(score.Accuracy, 2 + 2 * score.Accuracy)
+ bonusProportion) * modMultiplier);
break;
default:
return score.TotalScore;
convertedTotalScore = score.TotalScore;
break;
}
if (convertedTotalScore < 0)
throw new InvalidOperationException($"Total score conversion operation returned invalid total of {convertedTotalScore}");
return convertedTotalScore;
}
public static double ComputeAccuracy(ScoreInfo scoreInfo)
{
Ruleset ruleset = scoreInfo.Ruleset.CreateInstance();
ScoreProcessor scoreProcessor = ruleset.CreateScoreProcessor();
int baseScore = scoreInfo.Statistics.Where(kvp => kvp.Key.AffectsAccuracy())
.Sum(kvp => kvp.Value * scoreProcessor.GetBaseScoreForResult(kvp.Key));
int maxBaseScore = scoreInfo.MaximumStatistics.Where(kvp => kvp.Key.AffectsAccuracy())
.Sum(kvp => kvp.Value * scoreProcessor.GetBaseScoreForResult(kvp.Key));
return maxBaseScore == 0 ? 1 : baseScore / (double)maxBaseScore;
}
/// <summary>

Some files were not shown because too many files have changed in this diff Show More