1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 08:27:49 +08:00

Merge branch 'master' into fix-blueprint-end-placement

This commit is contained in:
Dean Herbert 2023-07-12 17:38:47 +09:00
commit b20190ed4f
18 changed files with 288 additions and 34 deletions

View File

@ -21,7 +21,7 @@
]
},
"ppy.localisationanalyser.tools": {
"version": "2022.809.0",
"version": "2023.712.0",
"commands": [
"localisation"
]

View File

@ -5,6 +5,7 @@ using System;
using NUnit.Framework;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Catch.Tests
@ -24,7 +25,8 @@ namespace osu.Game.Rulesets.Catch.Tests
new object[] { LegacyMods.HalfTime, new[] { typeof(CatchModHalfTime) } },
new object[] { LegacyMods.Flashlight, new[] { typeof(CatchModFlashlight) } },
new object[] { LegacyMods.Autoplay, new[] { typeof(CatchModAutoplay) } },
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(CatchModHardRock), typeof(CatchModDoubleTime) } }
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(CatchModHardRock), typeof(CatchModDoubleTime) } },
new object[] { LegacyMods.ScoreV2, new[] { typeof(ModScoreV2) } },
};
[TestCaseSource(nameof(catch_mod_mapping))]

View File

@ -91,6 +91,9 @@ namespace osu.Game.Rulesets.Catch
if (mods.HasFlagFast(LegacyMods.Relax))
yield return new CatchModRelax();
if (mods.HasFlagFast(LegacyMods.ScoreV2))
yield return new ModScoreV2();
}
public override IEnumerable<Mod> GetModsFor(ModType type)
@ -140,6 +143,12 @@ namespace osu.Game.Rulesets.Catch
new CatchModNoScope(),
};
case ModType.System:
return new Mod[]
{
new ModScoreV2(),
};
default:
return Array.Empty<Mod>();
}

View File

@ -5,6 +5,7 @@ using System;
using NUnit.Framework;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Mania.Tests
@ -36,7 +37,8 @@ namespace osu.Game.Rulesets.Mania.Tests
new object[] { LegacyMods.Key3, new[] { typeof(ManiaModKey3) } },
new object[] { LegacyMods.Key2, new[] { typeof(ManiaModKey2) } },
new object[] { LegacyMods.Mirror, new[] { typeof(ManiaModMirror) } },
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(ManiaModHardRock), typeof(ManiaModDoubleTime) } }
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(ManiaModHardRock), typeof(ManiaModDoubleTime) } },
new object[] { LegacyMods.ScoreV2, new[] { typeof(ModScoreV2) } },
};
[TestCaseSource(nameof(mania_mod_mapping))]

View File

@ -157,6 +157,9 @@ namespace osu.Game.Rulesets.Mania
if (mods.HasFlagFast(LegacyMods.Mirror))
yield return new ManiaModMirror();
if (mods.HasFlagFast(LegacyMods.ScoreV2))
yield return new ModScoreV2();
}
public override LegacyMods ConvertToLegacyMods(Mod[] mods)
@ -285,6 +288,12 @@ namespace osu.Game.Rulesets.Mania
new ModAdaptiveSpeed()
};
case ModType.System:
return new Mod[]
{
new ModScoreV2(),
};
default:
return Array.Empty<Mod>();
}

View File

@ -4,6 +4,7 @@
using System;
using NUnit.Framework;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Tests.Beatmaps;
@ -28,7 +29,8 @@ namespace osu.Game.Rulesets.Osu.Tests
new object[] { LegacyMods.SpunOut, new[] { typeof(OsuModSpunOut) } },
new object[] { LegacyMods.Autopilot, new[] { typeof(OsuModAutopilot) } },
new object[] { LegacyMods.Target, new[] { typeof(OsuModTargetPractice) } },
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(OsuModHardRock), typeof(OsuModDoubleTime) } }
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(OsuModHardRock), typeof(OsuModDoubleTime) } },
new object[] { LegacyMods.ScoreV2, new[] { typeof(ModScoreV2) } },
};
[TestCaseSource(nameof(osu_mod_mapping))]

View File

@ -113,6 +113,9 @@ namespace osu.Game.Rulesets.Osu
if (mods.HasFlagFast(LegacyMods.TouchDevice))
yield return new OsuModTouchDevice();
if (mods.HasFlagFast(LegacyMods.ScoreV2))
yield return new ModScoreV2();
}
public override LegacyMods ConvertToLegacyMods(Mod[] mods)
@ -212,6 +215,7 @@ namespace osu.Game.Rulesets.Osu
return new Mod[]
{
new OsuModTouchDevice(),
new ModScoreV2(),
};
default:

View File

@ -4,6 +4,7 @@
using System;
using NUnit.Framework;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Tests.Beatmaps;
@ -25,7 +26,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
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.HardRock | LegacyMods.DoubleTime, new[] { typeof(TaikoModHardRock), typeof(TaikoModDoubleTime) } },
new object[] { LegacyMods.ScoreV2, new[] { typeof(ModScoreV2) } },
};
[TestCaseSource(nameof(taiko_mod_mapping))]

View File

@ -116,6 +116,9 @@ namespace osu.Game.Rulesets.Taiko
if (mods.HasFlagFast(LegacyMods.Random))
yield return new TaikoModRandom();
if (mods.HasFlagFast(LegacyMods.ScoreV2))
yield return new ModScoreV2();
}
public override LegacyMods ConvertToLegacyMods(Mod[] mods)
@ -176,6 +179,12 @@ namespace osu.Game.Rulesets.Taiko
new ModAdaptiveSpeed()
};
case ModType.System:
return new Mod[]
{
new ModScoreV2(),
};
default:
return Array.Empty<Mod>();
}

View File

@ -0,0 +1,85 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Editing.Checks
{
public class CheckDrainTimeTest
{
private CheckDrainTime check = null!;
[SetUp]
public void Setup()
{
check = new CheckDrainTime();
}
[Test]
public void TestDrainTimeShort()
{
var beatmap = new Beatmap<HitObject>
{
HitObjects =
{
new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 29_999 }
}
};
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckDrainTime.IssueTemplateTooShort);
}
[Test]
public void TestDrainTimeBreak()
{
var beatmap = new Beatmap<HitObject>
{
HitObjects =
{
new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 40_000 }
},
Breaks = new List<BreakPeriod>
{
new BreakPeriod(10_000, 21_000)
}
};
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckDrainTime.IssueTemplateTooShort);
}
[Test]
public void TestDrainTimeCorrect()
{
var hitObjects = new List<HitObject>();
for (int i = 0; i <= 30; ++i)
hitObjects.Add(new HitCircle { StartTime = 1000 * i });
var beatmap = new Beatmap<HitObject> { HitObjects = hitObjects };
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
var issues = check.Run(context).ToList();
Assert.That(issues, Is.Empty);
}
}
}

View File

@ -38,6 +38,7 @@ namespace osu.Game.Beatmaps.Legacy
Key1 = 1 << 26,
Key3 = 1 << 27,
Key2 = 1 << 28,
ScoreV2 = 1 << 29,
Mirror = 1 << 30,
}
}

View File

@ -15,17 +15,19 @@ using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Development;
using osu.Framework.Extensions;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Input.Bindings;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Statistics;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Legacy;
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;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
@ -79,8 +81,9 @@ namespace osu.Game.Database
/// 29 2023-06-12 Run migration of old lazer scores to be best-effort in the new scoring number space. No actual realm changes.
/// 30 2023-06-16 Run migration of old lazer scores again. This time with more correct rounding considerations.
/// 31 2023-06-26 Add Version and LegacyTotalScore to ScoreInfo, set Version to 30000002 and copy TotalScore into LegacyTotalScore for legacy scores.
/// 32 2023-07-09 Populate legacy scores with the ScoreV2 mod (and restore TotalScore to the legacy total for such scores) using replay files.
/// </summary>
private const int schema_version = 31;
private const int schema_version = 32;
/// <summary>
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
@ -730,6 +733,8 @@ namespace osu.Game.Database
Logger.Log($"Running realm migration to version {targetVersion}...");
Stopwatch stopwatch = new Stopwatch();
var files = new RealmFileStore(this, storage);
stopwatch.Start();
switch (targetVersion)
@ -904,36 +909,17 @@ namespace osu.Game.Database
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
score.PopulateFromReplay(files, sr =>
{
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);
}
sr.ReadByte(); // Ruleset.
int version = sr.ReadInt32();
if (version < LegacyScoreEncoder.FIRST_LAZER_VERSION)
score.IsLegacyScore = true;
});
}
break;
@ -986,6 +972,46 @@ namespace osu.Game.Database
break;
}
case 32:
{
foreach (var score in migration.NewRealm.All<ScoreInfo>())
{
if (!score.IsLegacyScore || !score.Ruleset.IsLegacyRuleset())
continue;
score.PopulateFromReplay(files, sr =>
{
sr.ReadByte(); // Ruleset.
sr.ReadInt32(); // Version.
sr.ReadString(); // Beatmap hash.
sr.ReadString(); // Username.
sr.ReadString(); // MD5Hash.
sr.ReadUInt16(); // Count300.
sr.ReadUInt16(); // Count100.
sr.ReadUInt16(); // Count50.
sr.ReadUInt16(); // CountGeki.
sr.ReadUInt16(); // CountKatu.
sr.ReadUInt16(); // CountMiss.
// we should have this in LegacyTotalScore already, but if we're reading through this anyways...
int totalScore = sr.ReadInt32();
sr.ReadUInt16(); // Max combo.
sr.ReadBoolean(); // Perfect.
var legacyMods = (LegacyMods)sr.ReadInt32();
if (!legacyMods.HasFlagFast(LegacyMods.ScoreV2) || score.APIMods.Any(mod => mod.Acronym == @"SV2"))
return;
score.APIMods = score.APIMods.Append(new APIMod(new ModScoreV2())).ToArray();
score.LegacyTotalScore = score.TotalScore = totalScore;
});
}
break;
}
}
Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms");

View File

@ -5,10 +5,14 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Extensions;
using osu.Game.IO.Legacy;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
@ -205,6 +209,10 @@ namespace osu.Game.Database
if (ruleset is not ILegacyRuleset legacyRuleset)
return score.TotalScore;
var mods = score.Mods;
if (mods.Any(mod => mod is ModScoreV2))
return score.TotalScore;
var playableBeatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, score.Mods);
if (playableBeatmap.HitObjects.Count == 0)
@ -212,7 +220,7 @@ namespace osu.Game.Database
ILegacyScoreSimulator sv1Simulator = legacyRuleset.CreateLegacyScoreSimulator();
sv1Simulator.Simulate(beatmap, playableBeatmap, score.Mods);
sv1Simulator.Simulate(beatmap, playableBeatmap, mods);
return ConvertFromLegacyTotalScore(score, new DifficultyAttributes
{
@ -282,6 +290,38 @@ namespace osu.Game.Database
}
}
/// <summary>
/// Used to populate the <paramref name="score"/> model using data parsed from its corresponding replay file.
/// </summary>
/// <param name="score">The score to run population from replay for.</param>
/// <param name="files">A <see cref="RealmFileStore"/> instance to use for fetching replay.</param>
/// <param name="populationFunc">
/// Delegate describing the population to execute.
/// The delegate's argument is a <see cref="SerializationReader"/> instance which permits to read data from the replay stream.
/// </param>
public static void PopulateFromReplay(this ScoreInfo score, RealmFileStore files, Action<SerializationReader> populationFunc)
{
string? replayFilename = score.Files.FirstOrDefault(f => f.Filename.EndsWith(@".osr", StringComparison.InvariantCultureIgnoreCase))?.File.GetStoragePath();
if (replayFilename == null)
return;
try
{
using (var stream = files.Store.GetStream(replayFilename))
{
if (stream == null)
return;
using (SerializationReader sr = new SerializationReader(stream))
populationFunc.Invoke(sr);
}
}
catch (Exception e)
{
Logger.Error(e, $"Failed to read replay {replayFilename} during score migration", LoggingTarget.Database);
}
}
private class FakeHit : HitObject
{
private readonly Judgement judgement;

View File

@ -34,6 +34,7 @@ namespace osu.Game.Rulesets.Edit
new CheckUnsnappedObjects(),
new CheckConcurrentObjects(),
new CheckZeroLengthObjects(),
new CheckDrainTime(),
// Timing
new CheckPreviewTime(),

View File

@ -0,0 +1,38 @@
// 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 osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit.Checks.Components;
namespace osu.Game.Rulesets.Edit.Checks
{
public class CheckDrainTime : ICheck
{
private const int min_drain_threshold = 30 * 1000;
public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Compose, "Too short drain time");
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
{
new IssueTemplateTooShort(this)
};
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
double drainTime = context.Beatmap.CalculatePlayableLength() - context.Beatmap.TotalBreakTime;
if (drainTime < min_drain_threshold)
yield return new IssueTemplateTooShort(this).Create((int)(drainTime / 1000));
}
public class IssueTemplateTooShort : IssueTemplate
{
public IssueTemplateTooShort(ICheck check)
: base(check, IssueType.Problem, "Less than 30 seconds of drain time, currently {0}.")
{
}
public Issue Create(int drainTimeSeconds) => new Issue(this, drainTimeSeconds);
}
}
}

View File

@ -0,0 +1,20 @@
// 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.Localisation;
namespace osu.Game.Rulesets.Mods
{
/// <remarks>
/// This mod is used strictly to mark osu!stable scores set with the "Score V2" mod active.
/// It should not be used in any real capacity going forward.
/// </remarks>
public class ModScoreV2 : Mod
{
public override string Name => "Score V2";
public override string Acronym => @"SV2";
public override ModType Type => ModType.System;
public override LocalisableString Description => "Score set on earlier osu! versions with the V2 scoring algorithm active.";
public override double ScoreMultiplier => 1;
}
}

View File

@ -192,6 +192,10 @@ namespace osu.Game.Rulesets
case ModAutoplay:
value |= LegacyMods.Autoplay;
break;
case ModScoreV2:
value |= LegacyMods.ScoreV2;
break;
}
}

View File

@ -31,7 +31,7 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Toolkit.HighPerformance" Version="7.1.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="ppy.LocalisationAnalyser" Version="2022.809.0">
<PackageReference Include="ppy.LocalisationAnalyser" Version="2023.712.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>