mirror of
https://github.com/ppy/osu.git
synced 2026-05-14 03:42:36 +08:00
Compare commits
41 Commits
pp-dev
...
2023.614.1
+1
-1
@@ -11,7 +11,7 @@
|
||||
<AndroidManifestMerger>manifestmerger.jar</AndroidManifestMerger>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.531.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.608.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidManifestOverlay Include="$(MSBuildThisFileDirectory)osu.Android\Properties\AndroidManifestOverlay.xml" />
|
||||
|
||||
@@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
|
||||
speed => new SettingDescription(
|
||||
rawValue: speed,
|
||||
name: RulesetSettingsStrings.ScrollSpeed,
|
||||
value: RulesetSettingsStrings.ScrollSpeedTooltip(DrawableManiaRuleset.ComputeScrollTime(speed), speed)
|
||||
value: RulesetSettingsStrings.ScrollSpeedTooltip((int)DrawableManiaRuleset.ComputeScrollTime(speed), speed)
|
||||
)
|
||||
)
|
||||
};
|
||||
|
||||
@@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Mania
|
||||
|
||||
private partial class ManiaScrollSlider : RoundedSliderBar<int>
|
||||
{
|
||||
public override LocalisableString TooltipText => RulesetSettingsStrings.ScrollSpeedTooltip(DrawableManiaRuleset.ComputeScrollTime(Current.Value), Current.Value);
|
||||
public override LocalisableString TooltipText => RulesetSettingsStrings.ScrollSpeedTooltip((int)DrawableManiaRuleset.ComputeScrollTime(Current.Value), Current.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@ namespace osu.Game.Rulesets.Mania.Scoring
|
||||
|
||||
protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion)
|
||||
{
|
||||
return 200000 * comboProgress
|
||||
+ 800000 * Math.Pow(Accuracy.Value, 2 + 2 * Accuracy.Value) * accuracyProgress
|
||||
return 10000 * comboProgress
|
||||
+ 990000 * Math.Pow(Accuracy.Value, 2 + 2 * Accuracy.Value) * accuracyProgress
|
||||
+ bonusPortion;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,187 +15,175 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements
|
||||
[Test]
|
||||
public void TestHitAllDrumRoll()
|
||||
{
|
||||
const double hit_time = 1000;
|
||||
|
||||
PerformTest(new List<ReplayFrame>
|
||||
{
|
||||
new TaikoReplayFrame(0),
|
||||
new TaikoReplayFrame(1000, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(1001),
|
||||
new TaikoReplayFrame(1250, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(1251),
|
||||
new TaikoReplayFrame(1500, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(1501),
|
||||
new TaikoReplayFrame(1750, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(1751),
|
||||
new TaikoReplayFrame(2000, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(2001),
|
||||
}, CreateBeatmap(new DrumRoll
|
||||
{
|
||||
StartTime = hit_time,
|
||||
Duration = 1000
|
||||
}));
|
||||
}, CreateBeatmap(createDrumRoll(false)));
|
||||
|
||||
AssertJudgementCount(3);
|
||||
AssertJudgementCount(6);
|
||||
AssertResult<DrumRollTick>(0, HitResult.SmallBonus);
|
||||
AssertResult<DrumRollTick>(1, HitResult.SmallBonus);
|
||||
AssertResult<DrumRollTick>(2, HitResult.SmallBonus);
|
||||
AssertResult<DrumRollTick>(3, HitResult.SmallBonus);
|
||||
AssertResult<DrumRollTick>(4, HitResult.SmallBonus);
|
||||
AssertResult<DrumRoll>(0, HitResult.IgnoreHit);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHitSomeDrumRoll()
|
||||
{
|
||||
const double hit_time = 1000;
|
||||
|
||||
PerformTest(new List<ReplayFrame>
|
||||
{
|
||||
new TaikoReplayFrame(0),
|
||||
new TaikoReplayFrame(2000, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(2001),
|
||||
}, CreateBeatmap(new DrumRoll
|
||||
{
|
||||
StartTime = hit_time,
|
||||
Duration = 1000
|
||||
}));
|
||||
}, CreateBeatmap(createDrumRoll(false)));
|
||||
|
||||
AssertJudgementCount(3);
|
||||
AssertJudgementCount(6);
|
||||
AssertResult<DrumRollTick>(0, HitResult.IgnoreMiss);
|
||||
AssertResult<DrumRollTick>(1, HitResult.SmallBonus);
|
||||
AssertResult<DrumRollTick>(1, HitResult.IgnoreMiss);
|
||||
AssertResult<DrumRollTick>(2, HitResult.IgnoreMiss);
|
||||
AssertResult<DrumRollTick>(3, HitResult.IgnoreMiss);
|
||||
AssertResult<DrumRollTick>(4, HitResult.SmallBonus);
|
||||
AssertResult<DrumRoll>(0, HitResult.IgnoreHit);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHitNoneDrumRoll()
|
||||
{
|
||||
const double hit_time = 1000;
|
||||
|
||||
PerformTest(new List<ReplayFrame>
|
||||
{
|
||||
new TaikoReplayFrame(0),
|
||||
}, CreateBeatmap(new DrumRoll
|
||||
{
|
||||
StartTime = hit_time,
|
||||
Duration = 1000
|
||||
}));
|
||||
}, CreateBeatmap(createDrumRoll(false)));
|
||||
|
||||
AssertJudgementCount(3);
|
||||
AssertJudgementCount(6);
|
||||
AssertResult<DrumRollTick>(0, HitResult.IgnoreMiss);
|
||||
AssertResult<DrumRollTick>(1, HitResult.IgnoreMiss);
|
||||
AssertResult<DrumRollTick>(2, HitResult.IgnoreMiss);
|
||||
AssertResult<DrumRollTick>(3, HitResult.IgnoreMiss);
|
||||
AssertResult<DrumRollTick>(4, HitResult.IgnoreMiss);
|
||||
AssertResult<DrumRoll>(0, HitResult.IgnoreHit);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHitAllStrongDrumRollWithOneKey()
|
||||
{
|
||||
const double hit_time = 1000;
|
||||
|
||||
PerformTest(new List<ReplayFrame>
|
||||
{
|
||||
new TaikoReplayFrame(0),
|
||||
new TaikoReplayFrame(1000, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(1001),
|
||||
new TaikoReplayFrame(1250, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(1251),
|
||||
new TaikoReplayFrame(1500, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(1501),
|
||||
new TaikoReplayFrame(1750, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(1751),
|
||||
new TaikoReplayFrame(2000, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(2001),
|
||||
}, CreateBeatmap(new DrumRoll
|
||||
}, CreateBeatmap(createDrumRoll(true)));
|
||||
|
||||
AssertJudgementCount(12);
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
StartTime = hit_time,
|
||||
Duration = 1000,
|
||||
IsStrong = true
|
||||
}));
|
||||
|
||||
AssertJudgementCount(6);
|
||||
|
||||
AssertResult<DrumRollTick>(0, HitResult.SmallBonus);
|
||||
AssertResult<StrongNestedHitObject>(0, HitResult.LargeBonus);
|
||||
|
||||
AssertResult<DrumRollTick>(1, HitResult.SmallBonus);
|
||||
AssertResult<StrongNestedHitObject>(1, HitResult.LargeBonus);
|
||||
AssertResult<DrumRollTick>(i, HitResult.SmallBonus);
|
||||
AssertResult<StrongNestedHitObject>(i, HitResult.LargeBonus);
|
||||
}
|
||||
|
||||
AssertResult<DrumRoll>(0, HitResult.IgnoreHit);
|
||||
AssertResult<StrongNestedHitObject>(2, HitResult.IgnoreHit);
|
||||
AssertResult<StrongNestedHitObject>(5, HitResult.IgnoreHit);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHitSomeStrongDrumRollWithOneKey()
|
||||
{
|
||||
const double hit_time = 1000;
|
||||
|
||||
PerformTest(new List<ReplayFrame>
|
||||
{
|
||||
new TaikoReplayFrame(0),
|
||||
new TaikoReplayFrame(2000, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(2001),
|
||||
}, CreateBeatmap(new DrumRoll
|
||||
{
|
||||
StartTime = hit_time,
|
||||
Duration = 1000,
|
||||
IsStrong = true
|
||||
}));
|
||||
}, CreateBeatmap(createDrumRoll(true)));
|
||||
|
||||
AssertJudgementCount(6);
|
||||
AssertJudgementCount(12);
|
||||
|
||||
AssertResult<DrumRollTick>(0, HitResult.IgnoreMiss);
|
||||
AssertResult<StrongNestedHitObject>(0, HitResult.IgnoreMiss);
|
||||
|
||||
AssertResult<DrumRollTick>(1, HitResult.SmallBonus);
|
||||
AssertResult<StrongNestedHitObject>(1, HitResult.LargeBonus);
|
||||
AssertResult<DrumRollTick>(4, HitResult.SmallBonus);
|
||||
AssertResult<StrongNestedHitObject>(4, HitResult.LargeBonus);
|
||||
|
||||
AssertResult<DrumRoll>(0, HitResult.IgnoreHit);
|
||||
AssertResult<StrongNestedHitObject>(2, HitResult.IgnoreHit);
|
||||
AssertResult<StrongNestedHitObject>(5, HitResult.IgnoreHit);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHitAllStrongDrumRollWithBothKeys()
|
||||
{
|
||||
const double hit_time = 1000;
|
||||
|
||||
PerformTest(new List<ReplayFrame>
|
||||
{
|
||||
new TaikoReplayFrame(0),
|
||||
new TaikoReplayFrame(1000, TaikoAction.LeftCentre, TaikoAction.RightCentre),
|
||||
new TaikoReplayFrame(1001),
|
||||
new TaikoReplayFrame(1250, TaikoAction.LeftCentre, TaikoAction.RightCentre),
|
||||
new TaikoReplayFrame(1251),
|
||||
new TaikoReplayFrame(1500, TaikoAction.LeftCentre, TaikoAction.RightCentre),
|
||||
new TaikoReplayFrame(1501),
|
||||
new TaikoReplayFrame(1750, TaikoAction.LeftCentre, TaikoAction.RightCentre),
|
||||
new TaikoReplayFrame(1751),
|
||||
new TaikoReplayFrame(2000, TaikoAction.LeftCentre, TaikoAction.RightCentre),
|
||||
new TaikoReplayFrame(2001),
|
||||
}, CreateBeatmap(new DrumRoll
|
||||
}, CreateBeatmap(createDrumRoll(true)));
|
||||
|
||||
AssertJudgementCount(12);
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
StartTime = hit_time,
|
||||
Duration = 1000,
|
||||
IsStrong = true
|
||||
}));
|
||||
|
||||
AssertJudgementCount(6);
|
||||
|
||||
AssertResult<DrumRollTick>(0, HitResult.SmallBonus);
|
||||
AssertResult<StrongNestedHitObject>(0, HitResult.LargeBonus);
|
||||
|
||||
AssertResult<DrumRollTick>(1, HitResult.SmallBonus);
|
||||
AssertResult<StrongNestedHitObject>(1, HitResult.LargeBonus);
|
||||
AssertResult<DrumRollTick>(i, HitResult.SmallBonus);
|
||||
AssertResult<StrongNestedHitObject>(i, HitResult.LargeBonus);
|
||||
}
|
||||
|
||||
AssertResult<DrumRoll>(0, HitResult.IgnoreHit);
|
||||
AssertResult<StrongNestedHitObject>(2, HitResult.IgnoreHit);
|
||||
AssertResult<StrongNestedHitObject>(5, HitResult.IgnoreHit);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHitSomeStrongDrumRollWithBothKeys()
|
||||
{
|
||||
const double hit_time = 1000;
|
||||
|
||||
PerformTest(new List<ReplayFrame>
|
||||
{
|
||||
new TaikoReplayFrame(0),
|
||||
new TaikoReplayFrame(2000, TaikoAction.LeftCentre, TaikoAction.RightCentre),
|
||||
new TaikoReplayFrame(2001),
|
||||
}, CreateBeatmap(new DrumRoll
|
||||
{
|
||||
StartTime = hit_time,
|
||||
Duration = 1000,
|
||||
IsStrong = true
|
||||
}));
|
||||
}, CreateBeatmap(createDrumRoll(true)));
|
||||
|
||||
AssertJudgementCount(6);
|
||||
AssertJudgementCount(12);
|
||||
|
||||
AssertResult<DrumRollTick>(0, HitResult.IgnoreMiss);
|
||||
AssertResult<StrongNestedHitObject>(0, HitResult.IgnoreMiss);
|
||||
|
||||
AssertResult<DrumRollTick>(1, HitResult.SmallBonus);
|
||||
AssertResult<StrongNestedHitObject>(1, HitResult.LargeBonus);
|
||||
AssertResult<DrumRollTick>(4, HitResult.SmallBonus);
|
||||
AssertResult<StrongNestedHitObject>(4, HitResult.LargeBonus);
|
||||
|
||||
AssertResult<DrumRoll>(0, HitResult.IgnoreHit);
|
||||
AssertResult<StrongNestedHitObject>(2, HitResult.IgnoreHit);
|
||||
AssertResult<StrongNestedHitObject>(5, HitResult.IgnoreHit);
|
||||
}
|
||||
|
||||
private DrumRoll createDrumRoll(bool strong) => new DrumRoll
|
||||
{
|
||||
StartTime = 1000,
|
||||
Duration = 1000,
|
||||
IsStrong = strong
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,6 +92,14 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
// TODO: stable makes the last tick of a drumroll non-required when the next object is too close.
|
||||
// This probably needs to be reimplemented:
|
||||
//
|
||||
// List<HitObject> hitobjects = hitObjectManager.hitObjects;
|
||||
// int ind = hitobjects.IndexOf(this);
|
||||
// if (i < hitobjects.Count - 1 && hitobjects[i + 1].HittableStartTime - (EndTime + (int)TickSpacing) <= (int)TickSpacing)
|
||||
// lastTickHittable = false;
|
||||
|
||||
return converted;
|
||||
}
|
||||
|
||||
@@ -133,7 +141,6 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
|
||||
StartTime = obj.StartTime,
|
||||
Samples = obj.Samples,
|
||||
Duration = taikoDuration,
|
||||
TickRate = beatmap.Difficulty.SliderTickRate == 3 ? 3 : 4,
|
||||
SliderVelocity = obj is IHasSliderVelocity velocityData ? velocityData.SliderVelocity : 1
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using System.Threading;
|
||||
using osu.Framework.Bindables;
|
||||
@@ -69,6 +67,8 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
||||
double scoringDistance = base_distance * difficulty.SliderMultiplier * SliderVelocity;
|
||||
Velocity = scoringDistance / timingPoint.BeatLength;
|
||||
|
||||
TickRate = difficulty.SliderTickRate == 3 ? 3 : 4;
|
||||
|
||||
tickSpacing = timingPoint.BeatLength / TickRate;
|
||||
}
|
||||
|
||||
|
||||
@@ -209,10 +209,14 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
public override void TearDownSteps()
|
||||
{
|
||||
base.TearDownSteps();
|
||||
AddStep("delete imported", () =>
|
||||
AddStep("delete imported", () => Realm.Write(r =>
|
||||
{
|
||||
beatmaps.Delete(importedBeatmapSet);
|
||||
});
|
||||
// delete from realm directly rather than via `BeatmapManager` to avoid cross-test pollution
|
||||
// (`BeatmapManager.Delete()` uses soft deletion, which can lead to beatmap reuse between test cases).
|
||||
r.RemoveAll<BeatmapMetadata>();
|
||||
r.RemoveAll<BeatmapInfo>();
|
||||
r.RemoveAll<BeatmapSetInfo>();
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
|
||||
AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0)));
|
||||
AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
|
||||
AddStep("test gameplay", () => ((Editor)Game.ScreenStack.CurrentScreen).TestGameplay());
|
||||
AddStep("test gameplay", () => getEditor().TestGameplay());
|
||||
|
||||
AddUntilStep("wait for player", () =>
|
||||
{
|
||||
@@ -141,6 +141,37 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
AddUntilStep("wait for editor exit", () => Game.ScreenStack.CurrentScreen is not Editor);
|
||||
}
|
||||
|
||||
private EditorBeatmap getEditorBeatmap() => ((Editor)Game.ScreenStack.CurrentScreen).ChildrenOfType<EditorBeatmap>().Single();
|
||||
[Test]
|
||||
public void TestLastTimestampRememberedOnExit()
|
||||
{
|
||||
BeatmapSetInfo beatmapSet = null!;
|
||||
|
||||
AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely());
|
||||
AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach());
|
||||
|
||||
AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet));
|
||||
AddUntilStep("wait for song select",
|
||||
() => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
|
||||
&& Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
|
||||
&& songSelect.IsLoaded);
|
||||
|
||||
AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0)));
|
||||
AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
|
||||
|
||||
AddStep("seek to arbitrary time", () => getEditor().ChildrenOfType<EditorClock>().First().Seek(1234));
|
||||
AddUntilStep("time is correct", () => getEditor().ChildrenOfType<EditorClock>().First().CurrentTime, () => Is.EqualTo(1234));
|
||||
|
||||
AddStep("exit editor", () => InputManager.Key(Key.Escape));
|
||||
AddUntilStep("wait for editor exit", () => Game.ScreenStack.CurrentScreen is not Editor);
|
||||
|
||||
AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit());
|
||||
|
||||
AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
|
||||
AddUntilStep("time is correct", () => getEditor().ChildrenOfType<EditorClock>().First().CurrentTime, () => Is.EqualTo(1234));
|
||||
}
|
||||
|
||||
private EditorBeatmap getEditorBeatmap() => getEditor().ChildrenOfType<EditorBeatmap>().Single();
|
||||
|
||||
private Editor getEditor() => (Editor)Game.ScreenStack.CurrentScreen;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,6 +171,11 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
public double TimelineZoom { get; set; } = 1.0;
|
||||
|
||||
/// <summary>
|
||||
/// The time in milliseconds when last exiting the editor with this beatmap loaded.
|
||||
/// </summary>
|
||||
public double? EditorTimestamp { get; set; }
|
||||
|
||||
[Ignored]
|
||||
public CountdownType Countdown { get; set; } = CountdownType.Normal;
|
||||
|
||||
|
||||
@@ -22,12 +22,15 @@ using osu.Framework.Statistics;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.IO.Legacy;
|
||||
using osu.Game.Models;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Scoring.Legacy;
|
||||
using osu.Game.Skinning;
|
||||
using Realms;
|
||||
using Realms.Exceptions;
|
||||
@@ -71,8 +74,11 @@ namespace osu.Game.Database
|
||||
/// 24 2022-08-22 Added MaximumStatistics to ScoreInfo.
|
||||
/// 25 2022-09-18 Remove skins to add with new naming.
|
||||
/// 26 2023-02-05 Added BeatmapHash to ScoreInfo.
|
||||
/// 27 2023-06-06 Added EditorTimestamp to BeatmapInfo.
|
||||
/// 28 2023-06-08 Added IsLegacyScore to ScoreInfo, parsed from replay files.
|
||||
/// 29 2023-06-12 Run migration of old lazer scores to be best-effort in the new scoring number space. No actual realm changes.
|
||||
/// </summary>
|
||||
private const int schema_version = 26;
|
||||
private const int schema_version = 29;
|
||||
|
||||
/// <summary>
|
||||
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
|
||||
@@ -719,6 +725,11 @@ namespace osu.Game.Database
|
||||
|
||||
private void applyMigrationsForVersion(Migration migration, ulong targetVersion)
|
||||
{
|
||||
Logger.Log($"Running realm migration to version {targetVersion}...");
|
||||
Stopwatch stopwatch = new Stopwatch();
|
||||
|
||||
stopwatch.Start();
|
||||
|
||||
switch (targetVersion)
|
||||
{
|
||||
case 7:
|
||||
@@ -879,6 +890,7 @@ namespace osu.Game.Database
|
||||
break;
|
||||
|
||||
case 26:
|
||||
{
|
||||
// Add ScoreInfo.BeatmapHash property to ensure scores correspond to the correct version of beatmap.
|
||||
var scores = migration.NewRealm.All<ScoreInfo>();
|
||||
|
||||
@@ -886,7 +898,80 @@ namespace osu.Game.Database
|
||||
score.BeatmapHash = score.BeatmapInfo.Hash;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 28:
|
||||
{
|
||||
var files = new RealmFileStore(this, storage);
|
||||
var scores = migration.NewRealm.All<ScoreInfo>();
|
||||
|
||||
foreach (var score in scores)
|
||||
{
|
||||
string? replayFilename = score.Files.FirstOrDefault(f => f.Filename.EndsWith(@".osr", StringComparison.InvariantCultureIgnoreCase))?.File.GetStoragePath();
|
||||
if (replayFilename == null)
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
using (var stream = files.Store.GetStream(replayFilename))
|
||||
{
|
||||
if (stream == null)
|
||||
continue;
|
||||
|
||||
// Trimmed down logic from LegacyScoreDecoder to extract the version from replays.
|
||||
using (SerializationReader sr = new SerializationReader(stream))
|
||||
{
|
||||
sr.ReadByte(); // Ruleset.
|
||||
int version = sr.ReadInt32();
|
||||
if (version < LegacyScoreEncoder.FIRST_LAZER_VERSION)
|
||||
score.IsLegacyScore = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(e, $"Failed to read replay {replayFilename} during score migration", LoggingTarget.Database);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 29:
|
||||
{
|
||||
var scores = migration.NewRealm
|
||||
.All<ScoreInfo>()
|
||||
.Where(s => !s.IsLegacyScore);
|
||||
|
||||
foreach (var score in scores)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Recalculate the old-style standardised score to see if this was an old lazer score.
|
||||
bool oldScoreMatchesExpectations = StandardisedScoreMigrationTools.GetOldStandardised(score) == score.TotalScore;
|
||||
// Some older scores don't have correct statistics populated, so let's give them benefit of doubt.
|
||||
bool scoreIsVeryOld = score.Date < new DateTime(2023, 1, 1, 0, 0, 0);
|
||||
|
||||
if (oldScoreMatchesExpectations || scoreIsVeryOld)
|
||||
{
|
||||
try
|
||||
{
|
||||
long calculatedNew = StandardisedScoreMigrationTools.GetNewStandardised(score);
|
||||
score.TotalScore = calculatedNew;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms");
|
||||
}
|
||||
|
||||
private string? getRulesetShortNameFromLegacyID(long rulesetId)
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
public static class StandardisedScoreMigrationTools
|
||||
{
|
||||
public static long GetNewStandardised(ScoreInfo score)
|
||||
{
|
||||
int maxJudgementIndex = 0;
|
||||
|
||||
// Avoid retrieving from realm inside loops.
|
||||
int maxCombo = score.MaxCombo;
|
||||
|
||||
var ruleset = score.Ruleset.CreateInstance();
|
||||
var processor = ruleset.CreateScoreProcessor();
|
||||
|
||||
processor.TrackHitEvents = false;
|
||||
|
||||
var beatmap = new Beatmap();
|
||||
|
||||
HitResult maxRulesetJudgement = ruleset.GetHitResults().First().result;
|
||||
|
||||
// This is a list of all results, ordered from best to worst.
|
||||
// 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))
|
||||
.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))
|
||||
.SelectMany(kvp => Enumerable.Repeat(new FakeJudgement(kvp.Key), kvp.Value))
|
||||
.ToList();
|
||||
|
||||
// Some older scores may not have maximum statistics populated correctly.
|
||||
// In this case we need to fill them with best-known-defaults.
|
||||
if (maximumJudgements.Count != sortedHits.Count)
|
||||
{
|
||||
maximumJudgements = sortedHits
|
||||
.Select(r => new FakeJudgement(getMaxJudgementFor(r, maxRulesetJudgement)))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
// This is required to get the correct maximum combo portion.
|
||||
foreach (var judgement in maximumJudgements)
|
||||
beatmap.HitObjects.Add(new FakeHit(judgement));
|
||||
processor.ApplyBeatmap(beatmap);
|
||||
processor.Mods.Value = score.Mods;
|
||||
|
||||
// Insert all misses into a queue.
|
||||
// These will be nibbled at whenever we need to reset the combo.
|
||||
Queue<HitResult> misses = new Queue<HitResult>(score.Statistics
|
||||
.Where(kvp => kvp.Key == HitResult.Miss || kvp.Key == HitResult.LargeTickMiss)
|
||||
.SelectMany(kvp => Enumerable.Repeat(kvp.Key, kvp.Value)));
|
||||
|
||||
foreach (var result in sortedHits)
|
||||
{
|
||||
// For the main part of this loop, ignore all misses, as they will be inserted from the queue.
|
||||
if (result == HitResult.Miss || result == HitResult.LargeTickMiss)
|
||||
continue;
|
||||
|
||||
// Reset combo if required.
|
||||
if (processor.Combo.Value == maxCombo)
|
||||
insertMiss();
|
||||
|
||||
processor.ApplyResult(new JudgementResult(null!, maximumJudgements[maxJudgementIndex++])
|
||||
{
|
||||
Type = result
|
||||
});
|
||||
}
|
||||
|
||||
// Ensure we haven't forgotten any misses.
|
||||
while (misses.Count > 0)
|
||||
insertMiss();
|
||||
|
||||
var bonusHits = score.Statistics
|
||||
.Where(kvp => kvp.Key.IsBonus())
|
||||
.SelectMany(kvp => Enumerable.Repeat(kvp.Key, kvp.Value));
|
||||
|
||||
foreach (var result in bonusHits)
|
||||
processor.ApplyResult(new JudgementResult(null!, new FakeJudgement(result)) { Type = result });
|
||||
|
||||
// Not true for all scores for whatever reason. Oh well.
|
||||
// Debug.Assert(processor.HighestCombo.Value == score.MaxCombo);
|
||||
|
||||
return processor.TotalScore.Value;
|
||||
|
||||
void insertMiss()
|
||||
{
|
||||
if (misses.Count > 0)
|
||||
{
|
||||
processor.ApplyResult(new JudgementResult(null!, maximumJudgements[maxJudgementIndex++])
|
||||
{
|
||||
Type = misses.Dequeue(),
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// We ran out of misses. But we can't let max combo increase beyond the known value,
|
||||
// so let's forge a miss.
|
||||
processor.ApplyResult(new JudgementResult(null!, new FakeJudgement(getMaxJudgementFor(HitResult.Miss, maxRulesetJudgement)))
|
||||
{
|
||||
Type = HitResult.Miss,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static HitResult getMaxJudgementFor(HitResult hitResult, HitResult max)
|
||||
{
|
||||
switch (hitResult)
|
||||
{
|
||||
case HitResult.Miss:
|
||||
case HitResult.Meh:
|
||||
case HitResult.Ok:
|
||||
case HitResult.Good:
|
||||
case HitResult.Great:
|
||||
case HitResult.Perfect:
|
||||
return max;
|
||||
|
||||
case HitResult.SmallTickMiss:
|
||||
case HitResult.SmallTickHit:
|
||||
return HitResult.SmallTickHit;
|
||||
|
||||
case HitResult.LargeTickMiss:
|
||||
case HitResult.LargeTickHit:
|
||||
return HitResult.LargeTickHit;
|
||||
}
|
||||
|
||||
return HitResult.IgnoreHit;
|
||||
}
|
||||
|
||||
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 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 accuracyPortion = 0.3;
|
||||
|
||||
switch (score.RulesetID)
|
||||
{
|
||||
case 1:
|
||||
accuracyPortion = 0.75;
|
||||
break;
|
||||
|
||||
case 3:
|
||||
accuracyPortion = 0.99;
|
||||
break;
|
||||
}
|
||||
|
||||
double modMultiplier = 1;
|
||||
|
||||
foreach (var mod in score.Mods)
|
||||
modMultiplier *= mod.ScoreMultiplier;
|
||||
|
||||
return (long)((1000000 * (accuracyPortion * accuracyScore + (1 - accuracyPortion) * comboScore) + bonusScore) * modMultiplier);
|
||||
}
|
||||
|
||||
private class FakeHit : HitObject
|
||||
{
|
||||
private readonly Judgement judgement;
|
||||
|
||||
public override Judgement CreateJudgement() => judgement;
|
||||
|
||||
public FakeHit(Judgement judgement)
|
||||
{
|
||||
this.judgement = judgement;
|
||||
}
|
||||
}
|
||||
|
||||
private class FakeJudgement : Judgement
|
||||
{
|
||||
public override HitResult MaxResult { get; }
|
||||
|
||||
public FakeJudgement(HitResult maxResult)
|
||||
{
|
||||
MaxResult = maxResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,7 +82,7 @@ namespace osu.Game.Localisation
|
||||
/// <summary>
|
||||
/// "{0}ms (speed {1})"
|
||||
/// </summary>
|
||||
public static LocalisableString ScrollSpeedTooltip(double scrollTime, int scrollSpeed) => new TranslatableString(getKey(@"ruleset"), @"{0:0}ms (speed {1})", scrollTime, scrollSpeed);
|
||||
public static LocalisableString ScrollSpeedTooltip(int scrollTime, int scrollSpeed) => new TranslatableString(getKey(@"ruleset"), @"{0}ms (speed {1})", scrollTime, scrollSpeed);
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
|
||||
@@ -30,6 +30,11 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// </summary>
|
||||
protected int MaxHits { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether <see cref="SimulateAutoplay"/> is currently running.
|
||||
/// </summary>
|
||||
protected bool IsSimulating { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The total number of judged <see cref="HitObject"/>s at the current point in time.
|
||||
/// </summary>
|
||||
@@ -146,6 +151,8 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// <param name="beatmap">The <see cref="IBeatmap"/> to simulate.</param>
|
||||
protected virtual void SimulateAutoplay(IBeatmap beatmap)
|
||||
{
|
||||
IsSimulating = true;
|
||||
|
||||
foreach (var obj in beatmap.HitObjects)
|
||||
simulate(obj);
|
||||
|
||||
@@ -163,6 +170,8 @@ namespace osu.Game.Rulesets.Scoring
|
||||
result.Type = GetSimulatedHitResult(judgement);
|
||||
ApplyResult(result);
|
||||
}
|
||||
|
||||
IsSimulating = false;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
|
||||
@@ -30,6 +30,14 @@ namespace osu.Game.Rulesets.Scoring
|
||||
private const double accuracy_cutoff_c = 0.7;
|
||||
private const double accuracy_cutoff_d = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Whether <see cref="HitEvents"/> should be populated during application of results.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Should only be disabled for special cases.
|
||||
/// When disabled, <see cref="JudgementProcessor.RevertResult"/> cannot be used.</remarks>
|
||||
internal bool TrackHitEvents = true;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when this <see cref="ScoreProcessor"/> was reset from a replay frame.
|
||||
/// </summary>
|
||||
@@ -226,10 +234,16 @@ namespace osu.Game.Rulesets.Scoring
|
||||
|
||||
ApplyScoreChange(result);
|
||||
|
||||
hitEvents.Add(CreateHitEvent(result));
|
||||
lastHitObject = result.HitObject;
|
||||
if (!IsSimulating)
|
||||
{
|
||||
if (TrackHitEvents)
|
||||
{
|
||||
hitEvents.Add(CreateHitEvent(result));
|
||||
lastHitObject = result.HitObject;
|
||||
}
|
||||
|
||||
updateScore();
|
||||
updateScore();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -242,6 +256,9 @@ namespace osu.Game.Rulesets.Scoring
|
||||
|
||||
protected sealed override void RevertResultInternal(JudgementResult result)
|
||||
{
|
||||
if (!TrackHitEvents)
|
||||
throw new InvalidOperationException(@$"Rewind is not supported when {nameof(TrackHitEvents)} is disabled.");
|
||||
|
||||
Combo.Value = result.ComboAtJudgement;
|
||||
HighestCombo.Value = result.HighestComboAtJudgement;
|
||||
|
||||
@@ -311,6 +328,9 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// <param name="storeResults">Whether to store the current state of the <see cref="ScoreProcessor"/> for future use.</param>
|
||||
protected override void Reset(bool storeResults)
|
||||
{
|
||||
// Run one last time to store max values.
|
||||
updateScore();
|
||||
|
||||
base.Reset(storeResults);
|
||||
|
||||
hitEvents.Clear();
|
||||
|
||||
@@ -46,6 +46,9 @@ namespace osu.Game.Scoring.Legacy
|
||||
score.ScoreInfo = scoreInfo;
|
||||
|
||||
int version = sr.ReadInt32();
|
||||
|
||||
scoreInfo.IsLegacyScore = version < LegacyScoreEncoder.FIRST_LAZER_VERSION;
|
||||
|
||||
string beatmapHash = sr.ReadString();
|
||||
|
||||
workingBeatmap = GetBeatmap(beatmapHash);
|
||||
|
||||
@@ -28,9 +28,10 @@ namespace osu.Game.Scoring.Legacy
|
||||
/// <remarks>
|
||||
/// <list type="bullet">
|
||||
/// <item><description>30000001: Appends <see cref="LegacyReplaySoloScoreInfo"/> to the end of scores.</description></item>
|
||||
/// <item><description>30000002: Score stored to replay calculated using the Score V2 algorithm.</description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public const int LATEST_VERSION = 30000001;
|
||||
public const int LATEST_VERSION = 30000002;
|
||||
|
||||
/// <summary>
|
||||
/// The first stable-compatible YYYYMMDD format version given to lazer usage of replays.
|
||||
|
||||
@@ -16,7 +16,13 @@ namespace osu.Game.Scoring.Legacy
|
||||
=> getDisplayScore(scoreProcessor.Ruleset.RulesetInfo.OnlineID, scoreProcessor.TotalScore.Value, mode, scoreProcessor.MaximumStatistics);
|
||||
|
||||
public static long GetDisplayScore(this ScoreInfo scoreInfo, ScoringMode mode)
|
||||
=> getDisplayScore(scoreInfo.Ruleset.OnlineID, scoreInfo.TotalScore, mode, scoreInfo.MaximumStatistics);
|
||||
{
|
||||
// Temporary to not scale stable scores that are already in the XX-millions with the classic scoring mode.
|
||||
if (scoreInfo.IsLegacyScore)
|
||||
return scoreInfo.TotalScore;
|
||||
|
||||
return getDisplayScore(scoreInfo.Ruleset.OnlineID, scoreInfo.TotalScore, mode, scoreInfo.MaximumStatistics);
|
||||
}
|
||||
|
||||
private static long getDisplayScore(int rulesetId, long score, ScoringMode mode, IReadOnlyDictionary<HitResult, int> maximumStatistics)
|
||||
{
|
||||
|
||||
@@ -181,8 +181,7 @@ namespace osu.Game.Scoring
|
||||
/// <summary>
|
||||
/// Whether this <see cref="ScoreInfo"/> represents a legacy (osu!stable) score.
|
||||
/// </summary>
|
||||
[Ignored]
|
||||
public bool IsLegacyScore => Mods.OfType<ModClassic>().Any();
|
||||
public bool IsLegacyScore { get; set; }
|
||||
|
||||
private Dictionary<HitResult, int>? statistics;
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Input.Bindings;
|
||||
@@ -92,6 +93,9 @@ namespace osu.Game.Screens.Edit
|
||||
[Resolved(canBeNull: true)]
|
||||
private INotificationOverlay notifications { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private RealmAccess realm { get; set; }
|
||||
|
||||
public readonly Bindable<EditorScreenMode> Mode = new Bindable<EditorScreenMode>();
|
||||
|
||||
public IBindable<bool> SamplePlaybackDisabled => samplePlaybackDisabled;
|
||||
@@ -700,6 +704,13 @@ namespace osu.Game.Screens.Edit
|
||||
}
|
||||
}
|
||||
|
||||
realm.Write(r =>
|
||||
{
|
||||
var beatmap = r.Find<BeatmapInfo>(editorBeatmap.BeatmapInfo.ID);
|
||||
if (beatmap != null)
|
||||
beatmap.EditorTimestamp = clock.CurrentTime;
|
||||
});
|
||||
|
||||
ApplyToBackground(b =>
|
||||
{
|
||||
b.DimWhenUserSettingsIgnored.Value = 0;
|
||||
@@ -833,7 +844,11 @@ namespace osu.Game.Screens.Edit
|
||||
{
|
||||
double targetTime = 0;
|
||||
|
||||
if (Beatmap.Value.Beatmap.HitObjects.Count > 0)
|
||||
if (editorBeatmap.BeatmapInfo.EditorTimestamp != null)
|
||||
{
|
||||
targetTime = editorBeatmap.BeatmapInfo.EditorTimestamp.Value;
|
||||
}
|
||||
else if (Beatmap.Value.Beatmap.HitObjects.Count > 0)
|
||||
{
|
||||
// seek to one beat length before the first hitobject
|
||||
targetTime = Beatmap.Value.Beatmap.HitObjects[0].StartTime;
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Localisation.HUD;
|
||||
@@ -101,7 +102,12 @@ namespace osu.Game.Screens.Play.HUD
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
Height = bottom_bar_height + graph_height + handle_size.Y + info.Height - graph.Y;
|
||||
|
||||
// to prevent unnecessary invalidations of the song progress graph due to changes in size, apply tolerance when updating the height.
|
||||
float newHeight = bottom_bar_height + graph_height + handle_size.Y + info.Height - graph.Y;
|
||||
|
||||
if (!Precision.AlmostEquals(Height, newHeight, 5f))
|
||||
Height = newHeight;
|
||||
}
|
||||
|
||||
private void updateBarVisibility()
|
||||
|
||||
@@ -36,8 +36,8 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Realm" Version="10.20.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2023.531.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.510.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2023.608.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.605.0" />
|
||||
<PackageReference Include="Sentry" Version="3.28.1" />
|
||||
<PackageReference Include="SharpCompress" Version="0.32.2" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
|
||||
+1
-1
@@ -16,6 +16,6 @@
|
||||
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2023.531.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2023.608.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user