mirror of
https://github.com/ppy/osu.git
synced 2026-05-25 07:50:02 +08:00
Compare commits
183 Commits
@@ -46,22 +46,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.
|
||||

|
||||
|
||||
### Mobile platforms
|
||||
|
||||
@@ -69,10 +63,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
@@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.1213.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.1219.0" />
|
||||
</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();
|
||||
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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!;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,23 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
/// </remarks>
|
||||
public virtual void PlayAnimation()
|
||||
{
|
||||
switch (Result)
|
||||
if (Result.IsMiss())
|
||||
{
|
||||
default:
|
||||
JudgementText
|
||||
.FadeInFromZero(300, Easing.OutQuint)
|
||||
.ScaleTo(Vector2.One)
|
||||
.ScaleTo(new Vector2(1.2f), 1800, Easing.OutQuint);
|
||||
break;
|
||||
this.ScaleTo(1.6f);
|
||||
this.ScaleTo(1, 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, 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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
new object[] { LegacyMods.HalfTime, new[] { typeof(TaikoModHalfTime) } },
|
||||
new object[] { LegacyMods.Flashlight, new[] { typeof(TaikoModFlashlight) } },
|
||||
new object[] { LegacyMods.Autoplay, new[] { typeof(TaikoModAutoplay) } },
|
||||
new object[] { LegacyMods.Random, new[] { typeof(TaikoModRandom) } },
|
||||
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(TaikoModHardRock), typeof(TaikoModDoubleTime) } },
|
||||
new object[] { LegacyMods.ScoreV2, new[] { typeof(ModScoreV2) } },
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -115,23 +115,10 @@ namespace osu.Game.Rulesets.Taiko
|
||||
if (mods.HasFlagFast(LegacyMods.Relax))
|
||||
yield return new TaikoModRelax();
|
||||
|
||||
if (mods.HasFlagFast(LegacyMods.Random))
|
||||
yield return new TaikoModRandom();
|
||||
|
||||
if (mods.HasFlagFast(LegacyMods.ScoreV2))
|
||||
yield return new ModScoreV2();
|
||||
}
|
||||
|
||||
public override LegacyMods ConvertToLegacyMods(Mod[] mods)
|
||||
{
|
||||
var value = base.ConvertToLegacyMods(mods);
|
||||
|
||||
if (mods.OfType<TaikoModRandom>().Any())
|
||||
value |= LegacyMods.Random;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public override IEnumerable<Mod> GetModsFor(ModType type)
|
||||
{
|
||||
switch (type)
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -166,6 +166,29 @@ namespace osu.Game.Tests.Visual.Collections
|
||||
})));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCollectionNameCollisionsWithBuiltInItems()
|
||||
{
|
||||
AddStep("add dropdown", () =>
|
||||
{
|
||||
Add(new CollectionDropdown
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 0.4f,
|
||||
});
|
||||
});
|
||||
AddStep("add two collections which collide with default items", () => Realm.Write(r => r.Add(new[]
|
||||
{
|
||||
new BeatmapCollection(name: "All beatmaps"),
|
||||
new BeatmapCollection(name: "Manage collections...")
|
||||
{
|
||||
BeatmapMD5Hashes = { beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0].MD5Hash }
|
||||
},
|
||||
})));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRemoveCollectionViaButton()
|
||||
{
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ namespace osu.Game.Collections
|
||||
{
|
||||
if (changes == null)
|
||||
{
|
||||
filters.Clear();
|
||||
filters.Add(allBeatmapsItem);
|
||||
filters.AddRange(collections.Select(c => new CollectionFilterMenuItem(c.ToLive(realm))));
|
||||
if (ShowManageCollectionsItem)
|
||||
|
||||
@@ -37,22 +37,17 @@ namespace osu.Game.Collections
|
||||
CollectionName = name;
|
||||
}
|
||||
|
||||
public bool Equals(CollectionFilterMenuItem? other)
|
||||
public virtual bool Equals(CollectionFilterMenuItem? other)
|
||||
{
|
||||
if (other == null)
|
||||
return false;
|
||||
if (ReferenceEquals(null, other)) return false;
|
||||
if (ReferenceEquals(this, other)) return true;
|
||||
|
||||
// collections may have the same name, so compare first on reference equality.
|
||||
// this relies on the assumption that only one instance of the BeatmapCollection exists game-wide, managed by CollectionManager.
|
||||
if (Collection != null)
|
||||
return Collection.ID == other.Collection?.ID;
|
||||
if (Collection == null) return false;
|
||||
|
||||
// fallback to name-based comparison.
|
||||
// this is required for special dropdown items which don't have a collection (all beatmaps / manage collections items below).
|
||||
return CollectionName == other.CollectionName;
|
||||
return Collection.ID == other.Collection?.ID;
|
||||
}
|
||||
|
||||
public override int GetHashCode() => CollectionName.GetHashCode();
|
||||
public override int GetHashCode() => Collection?.ID.GetHashCode() ?? 0;
|
||||
}
|
||||
|
||||
public class AllBeatmapsCollectionFilterMenuItem : CollectionFilterMenuItem
|
||||
@@ -61,6 +56,10 @@ namespace osu.Game.Collections
|
||||
: base("All beatmaps")
|
||||
{
|
||||
}
|
||||
|
||||
public override bool Equals(CollectionFilterMenuItem? other) => other is AllBeatmapsCollectionFilterMenuItem;
|
||||
|
||||
public override int GetHashCode() => 1;
|
||||
}
|
||||
|
||||
public class ManageCollectionsFilterMenuItem : CollectionFilterMenuItem
|
||||
@@ -69,5 +68,9 @@ namespace osu.Game.Collections
|
||||
: base("Manage collections...")
|
||||
{
|
||||
}
|
||||
|
||||
public override bool Equals(CollectionFilterMenuItem? other) => other is ManageCollectionsFilterMenuItem;
|
||||
|
||||
public override int GetHashCode() => 2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -44,9 +44,6 @@ namespace osu.Game.Graphics.Containers
|
||||
|
||||
private bool shouldPerformRightMouseScroll(MouseButtonEvent e) => RightMouseScrollbar && e.Button == MouseButton.Right;
|
||||
|
||||
private void scrollFromMouseEvent(MouseEvent e) =>
|
||||
ScrollTo(Clamp(ToLocalSpace(e.ScreenSpaceMousePosition)[ScrollDim] / DrawSize[ScrollDim]) * Content.DrawSize[ScrollDim], true, DistanceDecayOnRightMouseScrollbar);
|
||||
|
||||
private bool rightMouseDragging;
|
||||
|
||||
protected override bool IsDragging => base.IsDragging || rightMouseDragging;
|
||||
@@ -80,7 +77,7 @@ namespace osu.Game.Graphics.Containers
|
||||
{
|
||||
if (shouldPerformRightMouseScroll(e))
|
||||
{
|
||||
scrollFromMouseEvent(e);
|
||||
ScrollFromMouseEvent(e);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -91,7 +88,7 @@ namespace osu.Game.Graphics.Containers
|
||||
{
|
||||
if (rightMouseDragging)
|
||||
{
|
||||
scrollFromMouseEvent(e);
|
||||
ScrollFromMouseEvent(e);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -129,6 +126,9 @@ namespace osu.Game.Graphics.Containers
|
||||
return base.OnScroll(e);
|
||||
}
|
||||
|
||||
protected virtual void ScrollFromMouseEvent(MouseEvent e) =>
|
||||
ScrollTo(Clamp(ToLocalSpace(e.ScreenSpaceMousePosition)[ScrollDim] / DrawSize[ScrollDim]) * Content.DrawSize[ScrollDim], true, DistanceDecayOnRightMouseScrollbar);
|
||||
|
||||
protected override ScrollbarContainer CreateScrollbar(Direction direction) => new OsuScrollbar(direction);
|
||||
|
||||
protected partial class OsuScrollbar : ScrollbarContainer
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Events;
|
||||
|
||||
namespace osu.Game.Graphics.Containers
|
||||
{
|
||||
@@ -46,6 +47,12 @@ namespace osu.Game.Graphics.Containers
|
||||
base.ScrollIntoView(target, animated);
|
||||
}
|
||||
|
||||
protected override void ScrollFromMouseEvent(MouseEvent e)
|
||||
{
|
||||
UserScrolling = true;
|
||||
base.ScrollFromMouseEvent(e);
|
||||
}
|
||||
|
||||
public new void ScrollTo(float value, bool animated = true, double? distanceDecay = null)
|
||||
{
|
||||
UserScrolling = false;
|
||||
|
||||
@@ -75,9 +75,12 @@ namespace osu.Game.Graphics
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case HitResult.IgnoreMiss:
|
||||
case HitResult.SmallTickMiss:
|
||||
case HitResult.LargeTickMiss:
|
||||
return Orange1;
|
||||
|
||||
case HitResult.Miss:
|
||||
case HitResult.LargeTickMiss:
|
||||
case HitResult.ComboBreak:
|
||||
return Red;
|
||||
|
||||
|
||||
@@ -397,7 +397,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
protected override DropdownSearchBar CreateSearchBar() => new OsuDropdownSearchBar
|
||||
{
|
||||
Padding = new MarginPadding { Right = 36 },
|
||||
Padding = new MarginPadding { Right = 26 },
|
||||
};
|
||||
|
||||
private partial class OsuDropdownSearchBar : DropdownSearchBar
|
||||
|
||||
@@ -160,6 +160,8 @@ namespace osu.Game.Input.Bindings
|
||||
new KeyBinding(InputKey.Enter, GlobalAction.ToggleChatFocus),
|
||||
new KeyBinding(InputKey.F1, GlobalAction.SaveReplay),
|
||||
new KeyBinding(InputKey.F2, GlobalAction.ExportReplay),
|
||||
new KeyBinding(InputKey.Plus, GlobalAction.IncreaseOffset),
|
||||
new KeyBinding(InputKey.Minus, GlobalAction.DecreaseOffset),
|
||||
};
|
||||
|
||||
private static IEnumerable<KeyBinding> replayKeyBindings => new[]
|
||||
@@ -404,6 +406,12 @@ namespace osu.Game.Input.Bindings
|
||||
|
||||
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleRotateControl))]
|
||||
EditorToggleRotateControl,
|
||||
|
||||
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.IncreaseOffset))]
|
||||
IncreaseOffset,
|
||||
|
||||
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.DecreaseOffset))]
|
||||
DecreaseOffset
|
||||
}
|
||||
|
||||
public enum GlobalActionCategory
|
||||
|
||||
@@ -344,6 +344,16 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString ExportReplay => new TranslatableString(getKey(@"export_replay"), @"Export replay");
|
||||
|
||||
/// <summary>
|
||||
/// "Increase offset"
|
||||
/// </summary>
|
||||
public static LocalisableString IncreaseOffset => new TranslatableString(getKey(@"increase_offset"), @"Increase offset");
|
||||
|
||||
/// <summary>
|
||||
/// "Decrease offset"
|
||||
/// </summary>
|
||||
public static LocalisableString DecreaseOffset => new TranslatableString(getKey(@"decrease_offset"), @"Decrease offset");
|
||||
|
||||
/// <summary>
|
||||
/// "Toggle rotate control"
|
||||
/// </summary>
|
||||
|
||||
@@ -97,8 +97,11 @@ namespace osu.Game.Online.Metadata
|
||||
{
|
||||
if (!connected.NewValue)
|
||||
{
|
||||
isWatchingUserPresence.Value = false;
|
||||
userStates.Clear();
|
||||
Schedule(() =>
|
||||
{
|
||||
isWatchingUserPresence.Value = false;
|
||||
userStates.Clear();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -187,13 +190,13 @@ namespace osu.Game.Online.Metadata
|
||||
|
||||
public override Task UserPresenceUpdated(int userId, UserPresence? presence)
|
||||
{
|
||||
lock (userStates)
|
||||
Schedule(() =>
|
||||
{
|
||||
if (presence != null)
|
||||
userStates[userId] = presence.Value;
|
||||
else
|
||||
userStates.Remove(userId);
|
||||
}
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
@@ -205,7 +208,7 @@ namespace osu.Game.Online.Metadata
|
||||
|
||||
Debug.Assert(connection != null);
|
||||
await connection.InvokeAsync(nameof(IMetadataServer.BeginWatchingUserPresence)).ConfigureAwait(false);
|
||||
isWatchingUserPresence.Value = true;
|
||||
Schedule(() => isWatchingUserPresence.Value = true);
|
||||
}
|
||||
|
||||
public override async Task EndWatchingUserPresence()
|
||||
@@ -215,14 +218,14 @@ namespace osu.Game.Online.Metadata
|
||||
if (connector?.IsConnected.Value != true)
|
||||
throw new OperationCanceledException();
|
||||
|
||||
// must happen synchronously before any remote calls to avoid misordering.
|
||||
userStates.Clear();
|
||||
// must be scheduled before any remote calls to avoid mis-ordering.
|
||||
Schedule(() => userStates.Clear());
|
||||
Debug.Assert(connection != null);
|
||||
await connection.InvokeAsync(nameof(IMetadataServer.EndWatchingUserPresence)).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
isWatchingUserPresence.Value = false;
|
||||
Schedule(() => isWatchingUserPresence.Value = false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -1190,7 +1190,7 @@ namespace osu.Game
|
||||
}
|
||||
else if (recentLogCount == short_term_display_limit)
|
||||
{
|
||||
string logFile = $@"{entry.Target.Value.ToString().ToLowerInvariant()}.log";
|
||||
string logFile = Logger.GetLogger(entry.Target.Value).Filename;
|
||||
|
||||
Schedule(() => Notifications.Post(new SimpleNotification
|
||||
{
|
||||
@@ -1198,7 +1198,7 @@ namespace osu.Game
|
||||
Text = NotificationsStrings.SubsequentMessagesLogged,
|
||||
Activated = () =>
|
||||
{
|
||||
Storage.GetStorageForDirectory(@"logs").PresentFileExternally(logFile);
|
||||
Logger.Storage.PresentFileExternally(logFile);
|
||||
return true;
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -527,14 +527,21 @@ namespace osu.Game
|
||||
{
|
||||
ManualResetEventSlim readyToRun = new ManualResetEventSlim();
|
||||
|
||||
bool success = false;
|
||||
|
||||
Scheduler.Add(() =>
|
||||
{
|
||||
realmBlocker = realm.BlockAllOperations("migration");
|
||||
try
|
||||
{
|
||||
realmBlocker = realm.BlockAllOperations("migration");
|
||||
success = true;
|
||||
}
|
||||
catch { }
|
||||
|
||||
readyToRun.Set();
|
||||
}, false);
|
||||
|
||||
if (!readyToRun.Wait(30000))
|
||||
if (!readyToRun.Wait(30000) || !success)
|
||||
throw new TimeoutException("Attempting to block for migration took too long.");
|
||||
|
||||
bool? cleanupSucceded = (Storage as OsuStorage)?.Migrate(Host.GetStorage(path));
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
@@ -218,6 +219,7 @@ namespace osu.Game.Overlays.Dashboard
|
||||
{
|
||||
panel.Anchor = Anchor.TopCentre;
|
||||
panel.Origin = Anchor.TopCentre;
|
||||
panel.CanSpectate.Value = playingUsers.Contains(user.Id);
|
||||
});
|
||||
|
||||
public partial class OnlineUserPanel : CompositeDrawable, IFilterable
|
||||
|
||||
@@ -132,6 +132,8 @@ namespace osu.Game.Overlays.Mods
|
||||
protected ShearedToggleButton? CustomisationButton { get; private set; }
|
||||
protected SelectAllModsButton? SelectAllModsButton { get; set; }
|
||||
|
||||
private bool textBoxShouldFocus;
|
||||
|
||||
private Sample? columnAppearSample;
|
||||
|
||||
private WorkingBeatmap? beatmap;
|
||||
@@ -508,6 +510,11 @@ namespace osu.Game.Overlays.Mods
|
||||
|
||||
modSettingsArea.ResizeHeightTo(modAreaHeight, transition_duration, Easing.InOutCubic);
|
||||
TopLevelContent.MoveToY(-modAreaHeight, transition_duration, Easing.InOutCubic);
|
||||
|
||||
if (customisationVisible.Value)
|
||||
SearchTextBox.KillFocus();
|
||||
else
|
||||
setTextBoxFocus(textBoxShouldFocus);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -621,8 +628,7 @@ namespace osu.Game.Overlays.Mods
|
||||
nonFilteredColumnCount += 1;
|
||||
}
|
||||
|
||||
if (textSearchStartsActive.Value)
|
||||
SearchTextBox.TakeFocus();
|
||||
setTextBoxFocus(textSearchStartsActive.Value);
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
@@ -761,14 +767,20 @@ namespace osu.Game.Overlays.Mods
|
||||
return false;
|
||||
|
||||
// TODO: should probably eventually support typical platform search shortcuts (`Ctrl-F`, `/`)
|
||||
if (SearchTextBox.HasFocus)
|
||||
SearchTextBox.KillFocus();
|
||||
else
|
||||
SearchTextBox.TakeFocus();
|
||||
|
||||
setTextBoxFocus(!textBoxShouldFocus);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void setTextBoxFocus(bool keepFocus)
|
||||
{
|
||||
textBoxShouldFocus = keepFocus;
|
||||
|
||||
if (textBoxShouldFocus)
|
||||
SearchTextBox.TakeFocus();
|
||||
else
|
||||
SearchTextBox.KillFocus();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Sample playback control
|
||||
|
||||
@@ -32,6 +32,8 @@ namespace osu.Game.Overlays.Mods
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
public override bool AcceptsFocus => true;
|
||||
|
||||
public ModSettingsArea()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
|
||||
@@ -54,7 +54,7 @@ namespace osu.Game.Overlays.Notifications
|
||||
set
|
||||
{
|
||||
text = value;
|
||||
Schedule(() => textDrawable.Text = text);
|
||||
Scheduler.AddOnce(t => textDrawable.Text = t, text);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -232,6 +232,14 @@ namespace osu.Game.Overlays.Profile.Header
|
||||
bool expanded = coverToggle.CoverExpanded.Value;
|
||||
|
||||
cover.ResizeHeightTo(expanded ? 250 : 0, transition_duration, Easing.OutQuint);
|
||||
|
||||
// Without this a very tiny slither of the cover will be visible even with a size of zero.
|
||||
// Integer masking woes, no doubt.
|
||||
if (expanded)
|
||||
cover.FadeIn(transition_duration, Easing.OutQuint);
|
||||
else
|
||||
cover.FadeOut(transition_duration, Easing.InQuint);
|
||||
|
||||
avatar.ResizeTo(new Vector2(expanded ? 120 : content_height), transition_duration, Easing.OutQuint);
|
||||
avatar.TransformTo(nameof(avatar.CornerRadius), expanded ? 40f : 20f, transition_duration, Easing.OutQuint);
|
||||
flow.TransformTo(nameof(flow.Spacing), new Vector2(expanded ? 20f : 10f), transition_duration, Easing.OutQuint);
|
||||
|
||||
@@ -136,9 +136,10 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
{
|
||||
base.Update();
|
||||
|
||||
Vector2 scale = drawable.DrawInfo.MatrixInverse.ExtractScale().Xy;
|
||||
drawableQuad = drawable.ToScreenSpace(
|
||||
drawable.DrawRectangle
|
||||
.Inflate(SkinSelectionHandler.INFLATE_SIZE));
|
||||
.Inflate(SkinSelectionHandler.INFLATE_SIZE * scale));
|
||||
|
||||
var localSpaceQuad = ToLocalSpace(drawableQuad);
|
||||
|
||||
|
||||
@@ -157,6 +157,15 @@ namespace osu.Game.Overlays.Toolbar
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
if (Hotkey != null)
|
||||
{
|
||||
realm.SubscribeToPropertyChanged(r => r.All<RealmKeyBinding>().FirstOrDefault(rkb => rkb.RulesetName == null && rkb.ActionInt == (int)Hotkey.Value), kb => kb.KeyCombinationString, updateKeyBindingTooltip);
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e) => false;
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
@@ -168,8 +177,6 @@ namespace osu.Game.Overlays.Toolbar
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
updateKeyBindingTooltip();
|
||||
|
||||
HoverBackground.FadeIn(200);
|
||||
tooltipContainer.FadeIn(100);
|
||||
|
||||
@@ -197,19 +204,13 @@ namespace osu.Game.Overlays.Toolbar
|
||||
{
|
||||
}
|
||||
|
||||
private void updateKeyBindingTooltip()
|
||||
private void updateKeyBindingTooltip(string keyCombination)
|
||||
{
|
||||
if (Hotkey == null) return;
|
||||
string keyBindingString = keyCombinationProvider.GetReadableString(keyCombination);
|
||||
|
||||
var realmKeyBinding = realm.Realm.All<RealmKeyBinding>().FirstOrDefault(rkb => rkb.RulesetName == null && rkb.ActionInt == (int)Hotkey.Value);
|
||||
|
||||
if (realmKeyBinding != null)
|
||||
{
|
||||
string keyBindingString = keyCombinationProvider.GetReadableString(realmKeyBinding.KeyCombination);
|
||||
|
||||
if (!string.IsNullOrEmpty(keyBindingString))
|
||||
keyBindingTooltip.Text = $" ({keyBindingString})";
|
||||
}
|
||||
keyBindingTooltip.Text = !string.IsNullOrEmpty(keyBindingString)
|
||||
? $" ({keyBindingString})"
|
||||
: string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,18 +38,16 @@ namespace osu.Game.Rulesets.Judgements
|
||||
/// </remarks>
|
||||
public virtual void PlayAnimation()
|
||||
{
|
||||
switch (Result)
|
||||
if (Result != HitResult.None && !Result.IsHit())
|
||||
{
|
||||
case HitResult.Miss:
|
||||
this.ScaleTo(1.6f);
|
||||
this.ScaleTo(1, 100, Easing.In);
|
||||
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);
|
||||
}
|
||||
|
||||
this.FadeOutFromOne(800);
|
||||
|
||||
@@ -133,12 +133,11 @@ namespace osu.Game.Rulesets.Judgements
|
||||
case HitResult.None:
|
||||
break;
|
||||
|
||||
case HitResult.Miss:
|
||||
ApplyMissAnimations();
|
||||
break;
|
||||
|
||||
default:
|
||||
ApplyHitAnimations();
|
||||
if (Result.Type.IsHit())
|
||||
ApplyHitAnimations();
|
||||
else
|
||||
ApplyMissAnimations();
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,16 +11,6 @@ namespace osu.Game.Rulesets.Judgements
|
||||
/// </summary>
|
||||
public class Judgement
|
||||
{
|
||||
/// <summary>
|
||||
/// The score awarded for a small bonus.
|
||||
/// </summary>
|
||||
public const int SMALL_BONUS_SCORE = 10;
|
||||
|
||||
/// <summary>
|
||||
/// The score awarded for a large bonus.
|
||||
/// </summary>
|
||||
public const int LARGE_BONUS_SCORE = 50;
|
||||
|
||||
/// <summary>
|
||||
/// The default health increase for a maximum judgement, as a proportion of total health.
|
||||
/// By default, each maximum judgement restores 5% of total health.
|
||||
@@ -91,23 +81,11 @@ namespace osu.Game.Rulesets.Judgements
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The numeric score representation for the maximum achievable result.
|
||||
/// </summary>
|
||||
public int MaxNumericResult => ToNumericResult(MaxResult);
|
||||
|
||||
/// <summary>
|
||||
/// The health increase for the maximum achievable result.
|
||||
/// </summary>
|
||||
public double MaxHealthIncrease => HealthIncreaseFor(MaxResult);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the numeric score representation of a <see cref="JudgementResult"/>.
|
||||
/// </summary>
|
||||
/// <param name="result">The <see cref="JudgementResult"/> to find the numeric score representation for.</param>
|
||||
/// <returns>The numeric score representation of <paramref name="result"/>.</returns>
|
||||
public int NumericResultFor(JudgementResult result) => ToNumericResult(result.Type);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the numeric health increase of a <see cref="HitResult"/>.
|
||||
/// </summary>
|
||||
@@ -165,41 +143,6 @@ namespace osu.Game.Rulesets.Judgements
|
||||
/// <returns>The numeric health increase of <paramref name="result"/>.</returns>
|
||||
public double HealthIncreaseFor(JudgementResult result) => HealthIncreaseFor(result.Type);
|
||||
|
||||
public override string ToString() => $"MaxResult:{MaxResult} MaxScore:{MaxNumericResult}";
|
||||
|
||||
public static int ToNumericResult(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:
|
||||
// Perfect doesn't actually give more score / accuracy directly.
|
||||
case HitResult.Perfect:
|
||||
return 300;
|
||||
|
||||
case HitResult.SmallBonus:
|
||||
return SMALL_BONUS_SCORE;
|
||||
|
||||
case HitResult.LargeBonus:
|
||||
return LARGE_BONUS_SCORE;
|
||||
}
|
||||
}
|
||||
public override string ToString() => $"MaxResult:{MaxResult}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,6 +112,6 @@ namespace osu.Game.Rulesets.Judgements
|
||||
RawTime = null;
|
||||
}
|
||||
|
||||
public override string ToString() => $"{Type} (Score:{Judgement.NumericResultFor(this)} HP:{Judgement.HealthIncreaseFor(this)} {Judgement})";
|
||||
public override string ToString() => $"{Type} ({Judgement})";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +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.Framework.Bindables;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Screens.Play;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
public abstract class ModBlockFail : Mod, IApplicableFailOverride, IApplicableToHUD, IReadFromConfig
|
||||
{
|
||||
private readonly Bindable<bool> showHealthBar = new Bindable<bool>();
|
||||
|
||||
/// <summary>
|
||||
/// We never fail, 'yo.
|
||||
/// </summary>
|
||||
public bool PerformFail() => false;
|
||||
|
||||
public bool RestartOnFail => false;
|
||||
|
||||
public void ReadFromConfig(OsuConfigManager config)
|
||||
{
|
||||
config.BindWith(OsuSetting.ShowHealthDisplayWhenCantFail, showHealthBar);
|
||||
}
|
||||
|
||||
public void ApplyToHUD(HUDOverlay overlay)
|
||||
{
|
||||
overlay.ShowHealthBar.BindTo(showHealthBar);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,14 +23,14 @@ namespace osu.Game.Rulesets.Mods
|
||||
}
|
||||
}
|
||||
|
||||
public class ModCinema : ModAutoplay, IApplicableToHUD, IApplicableToPlayer
|
||||
public class ModCinema : ModAutoplay, IApplicableToHUD, IApplicableToPlayer, IApplicableFailOverride
|
||||
{
|
||||
public override string Name => "Cinema";
|
||||
public override string Acronym => "CN";
|
||||
public override IconUsage? Icon => OsuIcon.ModCinema;
|
||||
public override LocalisableString Description => "Watch the video without visual distractions.";
|
||||
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModAutoplay)).ToArray();
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(ModAutoplay), typeof(ModNoFail) }).ToArray();
|
||||
|
||||
public void ApplyToHUD(HUDOverlay overlay)
|
||||
{
|
||||
@@ -45,5 +45,9 @@ namespace osu.Game.Rulesets.Mods
|
||||
|
||||
player.BreakOverlay.Hide();
|
||||
}
|
||||
|
||||
public bool PerformFail() => false;
|
||||
|
||||
public bool RestartOnFail => false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
|
||||
public override string Acronym => "CL";
|
||||
|
||||
public override double ScoreMultiplier => 0.5;
|
||||
public override double ScoreMultiplier => 0.96;
|
||||
|
||||
public override IconUsage? Icon => FontAwesome.Solid.History;
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Overlays.Settings;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
@@ -17,7 +18,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override ModType Type => ModType.DifficultyReduction;
|
||||
public override LocalisableString Description => "Whoaaaaa...";
|
||||
|
||||
[SettingSource("Speed decrease", "The actual decrease to apply")]
|
||||
[SettingSource("Speed decrease", "The actual decrease to apply", SettingControlType = typeof(MultiplierSettingsSlider))]
|
||||
public override BindableNumber<double> SpeedChange { get; } = new BindableDouble(0.75)
|
||||
{
|
||||
MinValue = 0.5,
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
public abstract class ModFailCondition : Mod, IApplicableToHealthProcessor, IApplicableFailOverride
|
||||
{
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModNoFail), typeof(ModRelax) };
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModNoFail) };
|
||||
|
||||
[SettingSource("Restart on fail", "Automatically restarts when failed.")]
|
||||
public BindableBool Restart { get; } = new BindableBool();
|
||||
|
||||
@@ -14,6 +14,7 @@ using osu.Game.Beatmaps.Timing;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Skinning;
|
||||
@@ -28,7 +29,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override ModType Type => ModType.DifficultyIncrease;
|
||||
public override LocalisableString Description => "Uguuuuuuuu...";
|
||||
|
||||
[SettingSource("Speed increase", "The actual increase to apply")]
|
||||
[SettingSource("Speed increase", "The actual increase to apply", SettingControlType = typeof(MultiplierSettingsSlider))]
|
||||
public override BindableNumber<double> SpeedChange { get; } = new BindableDouble(1.5)
|
||||
{
|
||||
MinValue = 1.01,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user