mirror of
https://github.com/ppy/osu.git
synced 2026-05-24 01:04:49 +08:00
Compare commits
106 Commits
@@ -136,8 +136,6 @@ When it comes to contributing to the project, the two main things you can do to
|
||||
|
||||
If you wish to help with localisation efforts, head over to [crowdin](https://crowdin.com/project/osu-web).
|
||||
|
||||
We love to reward quality contributions. If you have made a large contribution, or are a regular contributor, you are welcome to [submit an expense via opencollective](https://opencollective.com/ppy/expenses/new). If you have any questions, feel free to [reach out to peppy](mailto:pe@ppy.sh) before doing so.
|
||||
|
||||
Our team believes in **human contributions**. Any contribution – be it an issue report or a pull request – which is created by, documented by, or aided by AI/LLM usage will typically be **closed and locked without further discussion**.
|
||||
|
||||
## Licence
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2026.416.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2026.521.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
||||
@@ -20,13 +20,24 @@ using Uri = Android.Net.Uri;
|
||||
namespace osu.Android
|
||||
{
|
||||
[Activity(ConfigurationChanges = DEFAULT_CONFIG_CHANGES, Exported = true, LaunchMode = DEFAULT_LAUNCH_MODE, MainLauncher = true)]
|
||||
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")]
|
||||
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")]
|
||||
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osr", DataHost = "*", DataMimeType = "*/*")]
|
||||
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataMimeType = "application/x-osu-beatmap-archive")]
|
||||
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataMimeType = "application/x-osu-skin-archive")]
|
||||
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataMimeType = "application/x-osu-replay")]
|
||||
[IntentFilter(new[] { Intent.ActionSend, Intent.ActionSendMultiple }, Categories = new[] { Intent.CategoryDefault }, DataMimeTypes = new[]
|
||||
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, Label = "Import beatmap", DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*",
|
||||
DataMimeType = "*/*")]
|
||||
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, Label = "Import skin", DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*",
|
||||
DataMimeType = "*/*")]
|
||||
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, Label = "Import replay", DataScheme = "content", DataPathPattern = ".*\\\\.osr", DataHost = "*",
|
||||
DataMimeType = "*/*")]
|
||||
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, Label = "Import beatmap", DataScheme = "content", DataMimeType = "application/x-osu-beatmap-archive")]
|
||||
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, Label = "Import skin", DataScheme = "content", DataMimeType = "application/x-osu-skin-archive")]
|
||||
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, Label = "Import replay", DataScheme = "content", DataMimeType = "application/x-osu-replay")]
|
||||
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, Label = "Import file", DataScheme = "content", DataMimeTypes = new[]
|
||||
{
|
||||
"application/zip",
|
||||
"application/octet-stream",
|
||||
"application/download",
|
||||
"application/x-zip",
|
||||
"application/x-zip-compressed",
|
||||
})]
|
||||
[IntentFilter(new[] { Intent.ActionSend, Intent.ActionSendMultiple }, Categories = new[] { Intent.CategoryDefault }, Label = "Import", DataMimeTypes = new[]
|
||||
{
|
||||
"application/zip",
|
||||
"application/octet-stream",
|
||||
|
||||
@@ -190,7 +190,7 @@ namespace osu.Desktop
|
||||
}
|
||||
|
||||
// user party
|
||||
if (!hideIdentifiableInformation && multiplayerClient.Room != null && multiplayerClient.Room.Settings.MatchType != MatchType.Matchmaking)
|
||||
if (!hideIdentifiableInformation && multiplayerClient.Room != null && !multiplayerClient.Room.Settings.MatchType.IsMatchmakingType())
|
||||
{
|
||||
MultiplayerRoom room = multiplayerClient.Room;
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
// 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 Newtonsoft.Json;
|
||||
|
||||
namespace osu.Desktop.IPC.Messages
|
||||
{
|
||||
public class HitCountMessage : OsuWebSocketMessage
|
||||
{
|
||||
[JsonProperty("new_hits")]
|
||||
public long NewHits { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +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 Newtonsoft.Json;
|
||||
using osu.Framework.Extensions.TypeExtensions;
|
||||
|
||||
namespace osu.Desktop.IPC.Messages
|
||||
{
|
||||
public abstract class OsuWebSocketMessage
|
||||
{
|
||||
[JsonProperty("type")]
|
||||
public string Type { get; }
|
||||
|
||||
protected OsuWebSocketMessage()
|
||||
{
|
||||
Type = GetType().ReadableName();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// 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 System.Threading;
|
||||
using osu.Desktop.IPC.Messages;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.IPC;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using JsonConvert = Newtonsoft.Json.JsonConvert;
|
||||
|
||||
namespace osu.Desktop.IPC
|
||||
{
|
||||
public partial class OsuWebSocketProvider : Component
|
||||
{
|
||||
private WebSocketServer? server;
|
||||
private readonly Bindable<ScoreInfo> lastLocalScore = new Bindable<ScoreInfo>();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(SessionStatics sessionStatics)
|
||||
{
|
||||
server = new WebSocketServer(49727);
|
||||
server.StartAsync().FireAndForget(onError: ex => Logger.Error(ex, "Failed to start websocket"));
|
||||
|
||||
sessionStatics.BindWith(Static.LastLocalUserScore, lastLocalScore);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
lastLocalScore.BindValueChanged(val =>
|
||||
{
|
||||
if (val.NewValue == null)
|
||||
return;
|
||||
|
||||
if (server?.IsRunning != true)
|
||||
return;
|
||||
|
||||
var msg = new HitCountMessage { NewHits = val.NewValue.Statistics.Where(kv => kv.Key.IsBasic() && kv.Key.IsHit()).Sum(kv => kv.Value) };
|
||||
broadcast(msg);
|
||||
});
|
||||
}
|
||||
|
||||
private void broadcast(OsuWebSocketMessage message)
|
||||
{
|
||||
if (server?.IsRunning != true)
|
||||
return;
|
||||
|
||||
string messageString = JsonConvert.SerializeObject(message);
|
||||
server.BroadcastAsync(messageString).FireAndForget();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (server?.IsRunning == true)
|
||||
{
|
||||
var cts = new CancellationTokenSource();
|
||||
cts.CancelAfter(TimeSpan.FromSeconds(10));
|
||||
server.StopAsync(cts.Token).WaitSafely();
|
||||
server = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Versioning;
|
||||
using Microsoft.Win32;
|
||||
using osu.Desktop.IPC;
|
||||
using osu.Desktop.Performance;
|
||||
using osu.Desktop.Security;
|
||||
using osu.Framework.Platform;
|
||||
@@ -35,6 +36,8 @@ namespace osu.Desktop
|
||||
|
||||
public bool IsFirstRun { get; init; }
|
||||
|
||||
public bool EnableWebSocketServer { get; init; }
|
||||
|
||||
public OsuGameDesktop(string[]? args = null)
|
||||
: base(args)
|
||||
{
|
||||
@@ -148,6 +151,9 @@ namespace osu.Desktop
|
||||
|
||||
osuSchemeLinkIPCChannel = new OsuSchemeLinkIPCChannel(Host, this);
|
||||
archiveImportIPCChannel = new ArchiveImportIPCChannel(Host, this);
|
||||
|
||||
if (EnableWebSocketServer)
|
||||
Add(new OsuWebSocketProvider());
|
||||
}
|
||||
|
||||
public override void SetHost(GameHost host)
|
||||
|
||||
@@ -140,7 +140,8 @@ namespace osu.Desktop
|
||||
{
|
||||
host.Run(new OsuGameDesktop(args)
|
||||
{
|
||||
IsFirstRun = isFirstRun
|
||||
IsFirstRun = isFirstRun,
|
||||
EnableWebSocketServer = Environment.GetEnvironmentVariable("OSU_WEBSOCKET_SERVER") == "1",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="System.IO.Packaging" Version="10.0.5" />
|
||||
<PackageReference Include="DiscordRichPresence" Version="1.6.1.70" />
|
||||
<!-- Held back due to invite bug in newer versions. See https://github.com/Lachee/discord-rpc-csharp/issues/286-->
|
||||
<PackageReference Include="DiscordRichPresence" Version="1.5.0.51" />
|
||||
<PackageReference Include="Velopack" Version="0.0.1298" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Resources">
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
// 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 BenchmarkDotNet.Attributes;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Benchmarks
|
||||
{
|
||||
public class BenchmarkScoreMultiplierCalculator : BenchmarkTest
|
||||
{
|
||||
private ScoreMultiplierCalculator calculator = null!;
|
||||
|
||||
[Params(1, 10, 100)]
|
||||
public int Times { get; set; }
|
||||
|
||||
public record ModTestCase(string Description, IEnumerable<Mod> Mods)
|
||||
{
|
||||
public override string ToString() => Description;
|
||||
}
|
||||
|
||||
public static IEnumerable<ModTestCase> ValuesForMods =>
|
||||
[
|
||||
new ModTestCase("no mods", []),
|
||||
new ModTestCase("single mod", [new OsuModHardRock()]),
|
||||
new ModTestCase("single mod 2", [new OsuModEasy()]),
|
||||
new ModTestCase("multiple mods", [new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime()]),
|
||||
new ModTestCase("mods with adjusted settings", [
|
||||
new OsuModDoubleTime { SpeedChange = { Value = 2 } },
|
||||
new OsuModHidden { OnlyFadeApproachCircles = { Value = true } },
|
||||
new OsuModHardRock()
|
||||
]),
|
||||
];
|
||||
|
||||
[ParamsSource(nameof(ValuesForMods))]
|
||||
public ModTestCase Mods { get; set; } = null!;
|
||||
|
||||
public override void SetUp()
|
||||
{
|
||||
base.SetUp();
|
||||
calculator = new OsuRuleset().CreateScoreMultiplierCalculator();
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public double ViaModScoreMultiplier() => viaModScoreMultiplier(Times, Mods);
|
||||
|
||||
[Test]
|
||||
public void ViaModScoreMultiplier([Values(100)] int times, [ValueSource(nameof(ValuesForMods))] ModTestCase mods)
|
||||
=> viaModScoreMultiplier(times, mods);
|
||||
|
||||
private double viaModScoreMultiplier(int times, ModTestCase mods)
|
||||
{
|
||||
double scoreMultiplier = 1;
|
||||
|
||||
for (int i = 0; i < times; ++i)
|
||||
{
|
||||
scoreMultiplier = 1;
|
||||
|
||||
foreach (var mod in mods.Mods)
|
||||
scoreMultiplier *= mod.ScoreMultiplier;
|
||||
}
|
||||
|
||||
return scoreMultiplier;
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public double ViaCalculator()
|
||||
=> viaCalculator(Times, Mods);
|
||||
|
||||
[Test]
|
||||
public void ViaCalculator([Values(100)] int times, [ValueSource(nameof(ValuesForMods))] ModTestCase mods)
|
||||
=> viaCalculator(times, mods);
|
||||
|
||||
private double viaCalculator(int times, ModTestCase mods)
|
||||
{
|
||||
double scoreMultiplier = 1;
|
||||
|
||||
for (int i = 0; i < times; ++i)
|
||||
scoreMultiplier = calculator.CalculateFor(mods.Mods);
|
||||
|
||||
return scoreMultiplier;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.Rulesets.Catch.Mods;
|
||||
using osu.Game.Tests.Rulesets;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
public class CatchScoreMultiplierTest : RulesetScoreMultiplierTest
|
||||
{
|
||||
public CatchScoreMultiplierTest()
|
||||
: base(new CatchRuleset())
|
||||
{
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFlashlightOnNonDefaultSettings()
|
||||
=> TestModCombination([new CatchModFlashlight { ComboBasedSize = { Value = false } }]);
|
||||
|
||||
[Test]
|
||||
public void TestHalfTimeSpeeds([Values(0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99)] double speedChange)
|
||||
=> TestModCombination([new CatchModHalfTime { SpeedChange = { Value = speedChange } }]);
|
||||
|
||||
[Test]
|
||||
public void TestDaycoreSpeeds([Values(0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99)] double speedChange)
|
||||
=> TestModCombination([new CatchModDaycore { SpeedChange = { Value = speedChange } }]);
|
||||
|
||||
[Test]
|
||||
public void TestDoubleTimeSpeeds([Values(1.01, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3, 1.35, 1.4, 1.45, 1.5, 1.55, 1.6, 1.65, 1.7, 1.75, 1.8, 1.85, 1.9, 1.95, 2)] double speedChange)
|
||||
=> TestModCombination([new CatchModDoubleTime { SpeedChange = { Value = speedChange } }]);
|
||||
|
||||
[Test]
|
||||
public void TestNightcoreSpeeds([Values(1.01, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3, 1.35, 1.4, 1.45, 1.5, 1.55, 1.6, 1.65, 1.7, 1.75, 1.8, 1.85, 1.9, 1.95, 2)] double speedChange)
|
||||
=> TestModCombination([new CatchModNightcore { SpeedChange = { Value = speedChange } }]);
|
||||
|
||||
[Test]
|
||||
public void TestMultiplicativeCombination()
|
||||
=> TestModCombination([new CatchModHidden(), new CatchModHardRock()]);
|
||||
}
|
||||
}
|
||||
@@ -169,6 +169,8 @@ namespace osu.Game.Rulesets.Catch
|
||||
}
|
||||
}
|
||||
|
||||
public override ScoreMultiplierCalculator CreateScoreMultiplierCalculator() => new CatchScoreMultiplierCalculator();
|
||||
|
||||
public override string Description => "osu!catch";
|
||||
|
||||
public override string ShortName => SHORT_NAME;
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Catch.Edit.Blueprints;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Tools;
|
||||
|
||||
@@ -13,11 +13,11 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
public class BananaShowerCompositionTool : CompositionTool
|
||||
{
|
||||
public BananaShowerCompositionTool()
|
||||
: base(nameof(BananaShower))
|
||||
: base("Banana shower")
|
||||
{
|
||||
}
|
||||
|
||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners);
|
||||
public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.EditorBananaShower };
|
||||
|
||||
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new BananaShowerPlacementBlueprint();
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
private readonly List<ICheck> checks = new List<ICheck>
|
||||
{
|
||||
// Audio
|
||||
new CheckCatchFewHitsounds(),
|
||||
|
||||
// Compose
|
||||
new CheckBananaShowerGap(),
|
||||
new CheckConcurrentObjects(),
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
// 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.Edit.Checks;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit.Checks
|
||||
{
|
||||
public class CheckCatchFewHitsounds : CheckFewHitsounds
|
||||
{
|
||||
protected override bool IsExcludedFromHitsounding(HitObject hitObject) => hitObject is BananaShower;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,8 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Catch.Edit.Blueprints;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
@@ -17,7 +18,7 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
}
|
||||
|
||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles);
|
||||
public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.EditorFruit };
|
||||
|
||||
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new FruitPlacementBlueprint();
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Catch.Edit.Blueprints;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Tools;
|
||||
|
||||
@@ -13,11 +13,11 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
public class JuiceStreamCompositionTool : CompositionTool
|
||||
{
|
||||
public JuiceStreamCompositionTool()
|
||||
: base(nameof(JuiceStream))
|
||||
: base("Juice stream")
|
||||
{
|
||||
}
|
||||
|
||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders);
|
||||
public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.EditorJuiceStream };
|
||||
|
||||
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new JuiceStreamPlacementBlueprint();
|
||||
}
|
||||
|
||||
@@ -117,6 +117,15 @@ namespace osu.Game.Rulesets.Catch.Edit.Setup
|
||||
Beatmap.Difficulty.CircleSize = circleSizeSlider.Current.Value;
|
||||
Beatmap.Difficulty.DrainRate = healthDrainSlider.Current.Value;
|
||||
Beatmap.Difficulty.ApproachRate = approachRateSlider.Current.Value;
|
||||
// in lazer catch, Overall Difficulty does *nothing* - as it should be in a sane world.
|
||||
// in stable, it does *one extremely specific thing* which is influence the infamous `difficultyPeppyStars`
|
||||
// which in turn affects score V1 (see `LegacyRulesetExtensions.CalculateDifficultyPeppyStars()`).
|
||||
// there is a Ranking Criteria rule saying that Overall Difficulty and Approach Rate should match:
|
||||
// https://osu.ppy.sh/wiki/en/Ranking_criteria/osu!catch
|
||||
// the one case wherein that breaks stable is on some marathon maps;
|
||||
// on those setting Overall Difficulty too high can lead to score V1 exceeding 32 bits ("score overflow").
|
||||
// that case can be manually handled by mappers.
|
||||
Beatmap.Difficulty.OverallDifficulty = approachRateSlider.Current.Value;
|
||||
Beatmap.Difficulty.SliderMultiplier = baseVelocitySlider.Current.Value;
|
||||
Beatmap.Difficulty.SliderTickRate = tickRateSlider.Current.Value;
|
||||
|
||||
|
||||
@@ -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 osu.Game.Rulesets.Catch.Mods;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Scoring
|
||||
{
|
||||
public class CatchScoreMultiplierCalculator : ScoreMultiplierCalculator
|
||||
{
|
||||
static CatchScoreMultiplierCalculator()
|
||||
{
|
||||
#region Difficulty Reduction
|
||||
|
||||
Single<CatchModEasy>(hasMultiplier: 0.5);
|
||||
Single<CatchModNoFail>(hasMultiplier: 0.5);
|
||||
Single<CatchModHalfTime>(hasMultiplier: halfTime => rateAdjustMultiplier(halfTime.SpeedChange.Value));
|
||||
Single<CatchModDaycore>(hasMultiplier: daycore => rateAdjustMultiplier(daycore.SpeedChange.Value));
|
||||
|
||||
#endregion
|
||||
|
||||
#region Difficulty Increase
|
||||
|
||||
Single<CatchModHardRock>(hasMultiplier: hardRock => hardRock.UsesDefaultConfiguration ? 1.12 : 1);
|
||||
// Sudden Death
|
||||
// Perfect
|
||||
Single<CatchModDoubleTime>(hasMultiplier: doubleTime => rateAdjustMultiplier(doubleTime.SpeedChange.Value));
|
||||
Single<CatchModNightcore>(hasMultiplier: nightcore => rateAdjustMultiplier(nightcore.SpeedChange.Value));
|
||||
Single<CatchModHidden>(hasMultiplier: hidden => hidden.UsesDefaultConfiguration ? 1.06 : 1);
|
||||
Single<CatchModFlashlight>(hasMultiplier: flashlight => flashlight.UsesDefaultConfiguration ? 1.12 : 1);
|
||||
// Accuracy Challenge
|
||||
|
||||
#endregion
|
||||
|
||||
#region Conversion
|
||||
|
||||
Single<CatchModDifficultyAdjust>(hasMultiplier: 0.5);
|
||||
Single<CatchModClassic>(hasMultiplier: 0.96);
|
||||
// Mirror
|
||||
|
||||
#endregion
|
||||
|
||||
#region Automation
|
||||
|
||||
// Autoplay
|
||||
// Cinema
|
||||
Single<CatchModRelax>(hasMultiplier: 0.1);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Fun
|
||||
|
||||
Single<ModWindUp>(hasMultiplier: 0.5);
|
||||
Single<ModWindDown>(hasMultiplier: 0.5);
|
||||
// Floating Fruits
|
||||
// Muted
|
||||
// No Scope
|
||||
// Moving Fast
|
||||
Single<CatchModSynesthesia>(hasMultiplier: 0.8);
|
||||
|
||||
#endregion
|
||||
|
||||
#region System
|
||||
|
||||
// Score V2
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
private static double rateAdjustMultiplier(double speedChange)
|
||||
{
|
||||
// Round to the nearest multiple of 0.1.
|
||||
double value = (int)(speedChange * 10) / 10.0;
|
||||
|
||||
// Offset back to 0.
|
||||
value -= 1;
|
||||
|
||||
if (speedChange >= 1)
|
||||
return 1 + value / 5;
|
||||
else
|
||||
return 0.6 + value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,10 +20,10 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
[Test]
|
||||
public void TestKeyCountChange()
|
||||
{
|
||||
FormSliderBar<float> keyCount = null!;
|
||||
FormSliderBar<int> keyCount = null!;
|
||||
|
||||
AddStep("go to setup screen", () => InputManager.Key(Key.F4));
|
||||
AddUntilStep("retrieve key count slider", () => keyCount = Editor.ChildrenOfType<SetupScreen>().Single().ChildrenOfType<FormSliderBar<float>>().First(), () => Is.Not.Null);
|
||||
AddUntilStep("retrieve key count slider", () => keyCount = Editor.ChildrenOfType<SetupScreen>().Single().ChildrenOfType<FormSliderBar<int>>().First(), () => Is.Not.Null);
|
||||
AddAssert("key count is 5", () => keyCount.Current.Value, () => Is.EqualTo(5));
|
||||
AddStep("change key count to 8", () =>
|
||||
{
|
||||
@@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
});
|
||||
AddUntilStep("dialog visible", () => Game.ChildrenOfType<IDialogOverlay>().SingleOrDefault()?.CurrentDialog, Is.InstanceOf<SaveAndReloadEditorDialog>);
|
||||
AddStep("refuse", () => InputManager.Key(Key.Number2));
|
||||
AddAssert("key count is 5", () => keyCount.Current.Value, () => Is.EqualTo(5));
|
||||
AddUntilStep("key count is 5", () => keyCount.Current.Value, () => Is.EqualTo(5));
|
||||
|
||||
AddStep("change key count to 8 again", () =>
|
||||
{
|
||||
@@ -41,5 +41,32 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
AddStep("acquiesce", () => InputManager.Key(Key.Number1));
|
||||
AddUntilStep("beatmap became 8K", () => Game.Beatmap.Value.BeatmapInfo.Difficulty.CircleSize, () => Is.EqualTo(8));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDualStagesChange()
|
||||
{
|
||||
FormCheckBox dualStages = null!;
|
||||
FormSliderBar<int> keyCount = null!;
|
||||
|
||||
AddStep("go to setup screen", () => InputManager.Key(Key.F4));
|
||||
AddUntilStep("retrieve dual stages checkbox", () => dualStages = Editor.ChildrenOfType<SetupScreen>().Single().ChildrenOfType<FormCheckBox>().First(), () => Is.Not.Null);
|
||||
AddUntilStep("retrieve key count slider", () => keyCount = Editor.ChildrenOfType<SetupScreen>().Single().ChildrenOfType<FormSliderBar<int>>().First(), () => Is.Not.Null);
|
||||
AddAssert("key count is 5", () => keyCount.Current.Value, () => Is.EqualTo(5));
|
||||
AddStep("set dual stages", () =>
|
||||
{
|
||||
dualStages.Current.Value = true;
|
||||
});
|
||||
AddUntilStep("dialog visible", () => Game.ChildrenOfType<IDialogOverlay>().SingleOrDefault()?.CurrentDialog, Is.InstanceOf<SaveAndReloadEditorDialog>);
|
||||
AddStep("refuse", () => InputManager.Key(Key.Number2));
|
||||
AddUntilStep("key count is 5", () => keyCount.Current.Value, () => Is.EqualTo(5));
|
||||
|
||||
AddStep("set dual stages again", () =>
|
||||
{
|
||||
dualStages.Current.Value = true;
|
||||
});
|
||||
AddUntilStep("dialog visible", () => Game.ChildrenOfType<IDialogOverlay>().Single().CurrentDialog, Is.InstanceOf<SaveAndReloadEditorDialog>);
|
||||
AddStep("acquiesce", () => InputManager.Key(Key.Number1));
|
||||
AddUntilStep("beatmap became 12K", () => Game.Beatmap.Value.BeatmapInfo.Difficulty.CircleSize, () => Is.EqualTo(12));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,28 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
AddAssert("time is unchanged", () => EditorClock.CurrentTime, () => Is.EqualTo(initialTime));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNoTwoObjectsAtSameTimeAndColumn()
|
||||
{
|
||||
AddStep("change seek setting to false", () => config.SetValue(OsuSetting.EditorAutoSeekOnPlacement, false));
|
||||
AddStep("clear beatmap", () => EditorBeatmap.Clear());
|
||||
|
||||
AddStep("select note placement tool", () => InputManager.Key(Key.Number2));
|
||||
AddStep("move mouse to centre of last column", () => InputManager.MoveMouseTo(this.ChildrenOfType<Column>().Last().ScreenSpaceDrawQuad.Centre));
|
||||
AddStep("place note", () => InputManager.Click(MouseButton.Left));
|
||||
AddAssert("beatmap has 1 object", () => EditorBeatmap.HitObjects, () => Has.Count.EqualTo(1));
|
||||
|
||||
AddStep("select note placement tool", () => InputManager.Key(Key.Number2));
|
||||
AddStep("move mouse to centre of first column", () => InputManager.MoveMouseTo(this.ChildrenOfType<Column>().First().ScreenSpaceDrawQuad.Centre));
|
||||
AddStep("place note", () => InputManager.Click(MouseButton.Left));
|
||||
AddAssert("beatmap has 2 objects", () => EditorBeatmap.HitObjects, () => Has.Count.EqualTo(2));
|
||||
|
||||
AddStep("select note placement tool", () => InputManager.Key(Key.Number2));
|
||||
AddStep("move mouse to centre of last column", () => InputManager.MoveMouseTo(this.ChildrenOfType<Column>().Last().ScreenSpaceDrawQuad.Centre));
|
||||
AddStep("place note", () => InputManager.Click(MouseButton.Left));
|
||||
AddAssert("beatmap has 2 objects", () => EditorBeatmap.HitObjects, () => Has.Count.EqualTo(2));
|
||||
}
|
||||
|
||||
private void placeObject()
|
||||
{
|
||||
AddStep("select note placement tool", () => InputManager.Key(Key.Number2));
|
||||
|
||||
@@ -36,8 +36,8 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
var decoded = DecodeFromLegacy(beatmaps_resource_store.GetStream($"Resources/Testing/Beatmaps/{name}.osu"), beatmaps_resource_store, name);
|
||||
var decodedAfterEncode = DecodeFromLegacy(EncodeToLegacy(decoded), beatmaps_resource_store, name);
|
||||
|
||||
Sort(decoded.beatmap);
|
||||
Sort(decodedAfterEncode.beatmap);
|
||||
Sort(decoded.Beatmap);
|
||||
Sort(decodedAfterEncode.Beatmap);
|
||||
|
||||
CompareBeatmaps(decoded, decodedAfterEncode);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// 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.Rulesets.Mania.Mods;
|
||||
using osu.Game.Tests.Rulesets;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
public class ManiaScoreMultiplierTest : RulesetScoreMultiplierTest
|
||||
{
|
||||
public ManiaScoreMultiplierTest()
|
||||
: base(new ManiaRuleset())
|
||||
{
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHalfTimeSpeeds([Values(0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99)] double speedChange)
|
||||
=> TestModCombination([new ManiaModHalfTime { SpeedChange = { Value = speedChange } }]);
|
||||
|
||||
[Test]
|
||||
public void TestDaycoreSpeeds([Values(0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99)] double speedChange)
|
||||
=> TestModCombination([new ManiaModDaycore { SpeedChange = { Value = speedChange } }]);
|
||||
|
||||
[Test]
|
||||
public void TestDoubleTimeSpeeds([Values(1.01, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3, 1.35, 1.4, 1.45, 1.5, 1.55, 1.6, 1.65, 1.7, 1.75, 1.8, 1.85, 1.9, 1.95, 2)] double speedChange)
|
||||
=> TestModCombination([new ManiaModDoubleTime { SpeedChange = { Value = speedChange } }]);
|
||||
|
||||
[Test]
|
||||
public void TestNightcoreSpeeds([Values(1.01, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3, 1.35, 1.4, 1.45, 1.5, 1.55, 1.6, 1.65, 1.7, 1.75, 1.8, 1.85, 1.9, 1.95, 2)] double speedChange)
|
||||
=> TestModCombination([new ManiaModNightcore { SpeedChange = { Value = speedChange } }]);
|
||||
|
||||
[Test]
|
||||
public void TestMultiplicativeCombination()
|
||||
=> TestModCombination([new ManiaModEasy(), new ManiaModKey4()]);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,8 @@ using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Skinning.Default;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
@@ -90,5 +92,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
private float getNoteHeight(Column resultPlayfield) =>
|
||||
resultPlayfield.ToScreenSpace(new Vector2(DefaultNotePiece.NOTE_HEIGHT)).Y -
|
||||
resultPlayfield.ToScreenSpace(Vector2.Zero).Y;
|
||||
|
||||
public override bool ReplacesExistingObject(HitObject existing)
|
||||
=> base.ReplacesExistingObject(existing) && HitObject.Column == ((IHasColumn)existing).Column;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Tools;
|
||||
using osu.Game.Rulesets.Mania.Edit.Blueprints;
|
||||
@@ -16,7 +17,7 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
{
|
||||
}
|
||||
|
||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders);
|
||||
public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.EditorHoldNote };
|
||||
|
||||
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new HoldNotePlacementBlueprint();
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Tools;
|
||||
using osu.Game.Rulesets.Mania.Edit.Blueprints;
|
||||
@@ -17,7 +18,7 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
{
|
||||
}
|
||||
|
||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles);
|
||||
public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.EditorNote };
|
||||
|
||||
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new NotePlacementBlueprint();
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Formats;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
@@ -19,13 +20,22 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
|
||||
{
|
||||
public override LocalisableString Title => EditorSetupStrings.DifficultyHeader;
|
||||
|
||||
private FormSliderBar<float> keyCountSlider { get; set; } = null!;
|
||||
private FormSliderBar<int> keyCountSlider { get; set; } = null!;
|
||||
private FormCheckBox dualStages { get; set; } = null!;
|
||||
private FormCheckBox specialStyle { get; set; } = null!;
|
||||
private FormSliderBar<float> healthDrainSlider { get; set; } = null!;
|
||||
private FormSliderBar<float> overallDifficultySlider { get; set; } = null!;
|
||||
private FormSliderBar<double> baseVelocitySlider { get; set; } = null!;
|
||||
private FormSliderBar<double> tickRateSlider { get; set; } = null!;
|
||||
|
||||
private readonly BindableInt singleStageKeyCount = new BindableInt
|
||||
{
|
||||
Default = (int)BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||
Precision = 1,
|
||||
};
|
||||
|
||||
private readonly BindableInt actualKeyCount = new BindableInt();
|
||||
|
||||
[Resolved]
|
||||
private Editor? editor { get; set; }
|
||||
|
||||
@@ -37,20 +47,19 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
keyCountSlider = new FormSliderBar<float>
|
||||
keyCountSlider = new FormSliderBar<int>
|
||||
{
|
||||
Caption = BeatmapsetsStrings.ShowStatsCsMania,
|
||||
HintText = "The number of columns in the beatmap",
|
||||
Current = new BindableFloat(Beatmap.Difficulty.CircleSize)
|
||||
{
|
||||
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Precision = 1,
|
||||
},
|
||||
Current = singleStageKeyCount,
|
||||
TransferValueOnCommit = true,
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
dualStages = new FormCheckBox
|
||||
{
|
||||
Caption = "Dual stages",
|
||||
HintText = "Doubles the number of keys by adding a second stage."
|
||||
},
|
||||
specialStyle = new FormCheckBox
|
||||
{
|
||||
Caption = "Use special (N+1) style",
|
||||
@@ -117,16 +126,54 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
|
||||
},
|
||||
};
|
||||
|
||||
keyCountSlider.Current.BindValueChanged(updateKeyCount);
|
||||
setStateFromActualKeyCount((int)Beatmap.Difficulty.CircleSize);
|
||||
|
||||
keyCountSlider.Current.BindValueChanged(_ => calculateActualKeyCount());
|
||||
dualStages.Current.BindValueChanged(_ =>
|
||||
{
|
||||
updateSingleStageKeyCountBounds();
|
||||
calculateActualKeyCount();
|
||||
});
|
||||
actualKeyCount.BindValueChanged(updateKeyCount);
|
||||
|
||||
healthDrainSlider.Current.BindValueChanged(_ => updateValues());
|
||||
overallDifficultySlider.Current.BindValueChanged(_ => updateValues());
|
||||
baseVelocitySlider.Current.BindValueChanged(_ => updateValues());
|
||||
tickRateSlider.Current.BindValueChanged(_ => updateValues());
|
||||
}
|
||||
|
||||
private void updateSingleStageKeyCountBounds()
|
||||
{
|
||||
singleStageKeyCount.MinValue = dualStages.Current.Value ? ManiaRuleset.MAX_STAGE_KEYS / 2 + 1 : 1;
|
||||
singleStageKeyCount.MaxValue = dualStages.Current.Value ? LegacyBeatmapDecoder.MAX_MANIA_KEY_COUNT / 2 : ManiaRuleset.MAX_STAGE_KEYS;
|
||||
}
|
||||
|
||||
private void setStateFromActualKeyCount(int keyCount)
|
||||
{
|
||||
actualKeyCount.Value = keyCount;
|
||||
|
||||
if (keyCount > 10)
|
||||
{
|
||||
dualStages.Current.Value = true;
|
||||
singleStageKeyCount.Value = keyCount / 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
dualStages.Current.Value = false;
|
||||
singleStageKeyCount.Value = keyCount;
|
||||
}
|
||||
|
||||
updateSingleStageKeyCountBounds();
|
||||
}
|
||||
|
||||
private void calculateActualKeyCount()
|
||||
{
|
||||
actualKeyCount.Value = keyCountSlider.Current.Value * (dualStages.Current.Value ? 2 : 1);
|
||||
}
|
||||
|
||||
private bool updatingKeyCount;
|
||||
|
||||
private void updateKeyCount(ValueChangedEvent<float> keyCount)
|
||||
private void updateKeyCount(ValueChangedEvent<int> keyCount)
|
||||
{
|
||||
if (updatingKeyCount) return;
|
||||
|
||||
@@ -143,7 +190,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
|
||||
Schedule(() =>
|
||||
{
|
||||
changeHandler!.RestoreState(-1);
|
||||
Beatmap.Difficulty.CircleSize = keyCountSlider.Current.Value = keyCount.OldValue;
|
||||
Beatmap.Difficulty.CircleSize = keyCount.OldValue;
|
||||
setStateFromActualKeyCount(keyCount.OldValue);
|
||||
updatingKeyCount = false;
|
||||
});
|
||||
}
|
||||
@@ -158,7 +206,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
|
||||
{
|
||||
// for now, update these on commit rather than making BeatmapMetadata bindables.
|
||||
// after switching database engines we can reconsider if switching to bindables is a good direction.
|
||||
Beatmap.Difficulty.CircleSize = keyCountSlider.Current.Value;
|
||||
Beatmap.Difficulty.CircleSize = actualKeyCount.Value;
|
||||
Beatmap.SpecialStyle = specialStyle.Current.Value;
|
||||
Beatmap.Difficulty.DrainRate = healthDrainSlider.Current.Value;
|
||||
Beatmap.Difficulty.OverallDifficulty = overallDifficultySlider.Current.Value;
|
||||
|
||||
@@ -307,6 +307,8 @@ namespace osu.Game.Rulesets.Mania
|
||||
}
|
||||
}
|
||||
|
||||
public override ScoreMultiplierCalculator CreateScoreMultiplierCalculator() => new ManiaScoreMultiplierCalculator();
|
||||
|
||||
public override string Description => "osu!mania";
|
||||
|
||||
public override string ShortName => SHORT_NAME;
|
||||
@@ -485,6 +487,22 @@ namespace osu.Game.Rulesets.Mania
|
||||
};
|
||||
}
|
||||
|
||||
public override IEnumerable<RulesetBeatmapAttribute> GetBeatmapAttributesForRankedPlayCard(IBeatmapInfo beatmapInfo, IReadOnlyCollection<Mod> mods)
|
||||
{
|
||||
var attributes = GetBeatmapAttributesForDisplay(beatmapInfo, mods).ToList();
|
||||
|
||||
// Key count attribute isn't relevant to ranked play (it's decided by the pool).
|
||||
attributes.RemoveAll(a => a.Acronym == "KC");
|
||||
|
||||
float holdNoteRatio = beatmapInfo.TotalObjectCount == 0 ? 0 : (float)beatmapInfo.EndTimeObjectCount / beatmapInfo.TotalObjectCount;
|
||||
attributes.Insert(0, new RulesetBeatmapAttribute("Hold notes", @"HN", holdNoteRatio, holdNoteRatio, 1)
|
||||
{
|
||||
ValueFormat = "P0"
|
||||
});
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
public override IRulesetFilterCriteria CreateRulesetFilterCriteria()
|
||||
{
|
||||
return new ManiaFilterCriteria();
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
// 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.Mania.Mods;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Scoring
|
||||
{
|
||||
public class ManiaScoreMultiplierCalculator : ScoreMultiplierCalculator
|
||||
{
|
||||
static ManiaScoreMultiplierCalculator()
|
||||
{
|
||||
#region Difficulty Reduction
|
||||
|
||||
Single<ManiaModEasy>(hasMultiplier: 0.5);
|
||||
Single<ManiaModNoFail>(hasMultiplier: 0.5);
|
||||
Single<ManiaModHalfTime>(hasMultiplier: halfTime => rateAdjustMultiplier(halfTime.SpeedChange.Value));
|
||||
Single<ManiaModDaycore>(hasMultiplier: daycore => rateAdjustMultiplier(daycore.SpeedChange.Value));
|
||||
Single<ManiaModNoRelease>(hasMultiplier: 0.9);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Difficulty Increase
|
||||
|
||||
// Hard Rock
|
||||
// Sudden Death
|
||||
// Perfect
|
||||
// Double Time
|
||||
// Nightcore
|
||||
// Fade In
|
||||
// Hidden
|
||||
// Cover
|
||||
// Flashlight
|
||||
// Accuracy Challenge
|
||||
|
||||
#endregion
|
||||
|
||||
#region Conversion
|
||||
|
||||
// Random
|
||||
// Dual Stages
|
||||
// Mirror
|
||||
Single<ManiaModDifficultyAdjust>(hasMultiplier: 0.5);
|
||||
Single<ManiaModClassic>(hasMultiplier: 0.96);
|
||||
// Invert
|
||||
Single<ManiaModConstantSpeed>(hasMultiplier: 0.9);
|
||||
Single<ManiaModHoldOff>(hasMultiplier: 0.9);
|
||||
Single<ManiaModKey1>(hasMultiplier: 0.9);
|
||||
Single<ManiaModKey2>(hasMultiplier: 0.9);
|
||||
Single<ManiaModKey3>(hasMultiplier: 0.9);
|
||||
Single<ManiaModKey4>(hasMultiplier: 0.9);
|
||||
Single<ManiaModKey5>(hasMultiplier: 0.9);
|
||||
Single<ManiaModKey6>(hasMultiplier: 0.9);
|
||||
Single<ManiaModKey7>(hasMultiplier: 0.9);
|
||||
Single<ManiaModKey8>(hasMultiplier: 0.9);
|
||||
Single<ManiaModKey9>(hasMultiplier: 0.9);
|
||||
Single<ManiaModKey10>(hasMultiplier: 0.9);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Automation
|
||||
|
||||
// Autoplay
|
||||
// Cinema
|
||||
|
||||
#endregion
|
||||
|
||||
#region Fun
|
||||
|
||||
Single<ModWindUp>(hasMultiplier: 0.5);
|
||||
Single<ModWindDown>(hasMultiplier: 0.5);
|
||||
// Muted
|
||||
Single<ModAdaptiveSpeed>(hasMultiplier: 0.5);
|
||||
|
||||
#endregion
|
||||
|
||||
#region System
|
||||
|
||||
// Score V2
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
private static double rateAdjustMultiplier(double speedChange)
|
||||
{
|
||||
// Round to the nearest multiple of 0.1.
|
||||
double value = (int)(speedChange * 10) / 10.0;
|
||||
|
||||
// Offset back to 0.
|
||||
value -= 1;
|
||||
|
||||
if (speedChange >= 1)
|
||||
return 1 + value / 5;
|
||||
else
|
||||
return 0.6 + value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,6 +73,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
|
||||
AddStep("end slider placement", () => InputManager.Click(MouseButton.Right));
|
||||
|
||||
AddStep("seek to slider end", () =>
|
||||
{
|
||||
var slider = (Slider)EditorBeatmap.HitObjects.Single();
|
||||
EditorClock.Seek(slider.EndTime);
|
||||
});
|
||||
|
||||
AddStep("enter circle placement mode", () => InputManager.Key(Key.Number2));
|
||||
|
||||
AddStep("move mouse slightly", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.205f, 0)));
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
[Test]
|
||||
public void TestTouchInputPlaceHitCircleDirectly()
|
||||
{
|
||||
AddStep("tap circle", () => tap(this.ChildrenOfType<EditorRadioButton>().Single(b => b.Button.Label == "HitCircle")));
|
||||
AddStep("tap circle", () => tap(this.ChildrenOfType<EditorRadioButton>().Single(b => b.Button.Label == "Hit circle")));
|
||||
|
||||
AddStep("tap to place circle", () => tap(this.ChildrenOfType<Playfield>().Single()));
|
||||
AddAssert("circle placed correctly", () =>
|
||||
@@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
[Test]
|
||||
public void TestTouchInputPlaceCircleAfterTouchingComposeArea()
|
||||
{
|
||||
AddStep("tap circle", () => tap(this.ChildrenOfType<EditorRadioButton>().Single(b => b.Button.Label == "HitCircle")));
|
||||
AddStep("tap circle", () => tap(this.ChildrenOfType<EditorRadioButton>().Single(b => b.Button.Label == "Hit circle")));
|
||||
|
||||
AddStep("tap playfield", () => tap(this.ChildrenOfType<Playfield>().Single()));
|
||||
AddAssert("circle placed", () => EditorBeatmap.HitObjects.Single(h => h.StartTime == EditorClock.CurrentTimeAccurate) is HitCircle);
|
||||
|
||||
@@ -7,6 +7,9 @@ using osu.Framework.Input;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Osu.Edit;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Edit;
|
||||
@@ -130,5 +133,74 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
AddAssert("slider has correct velocity", () => slider!.Velocity, () => Is.EqualTo(velocityBefore));
|
||||
AddAssert("slider has correct duration", () => slider!.Duration, () => Is.EqualTo(durationBefore));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestVelocityToolbox()
|
||||
{
|
||||
ExpandableSlider<double> velocitySlider = null!;
|
||||
ExpandableButton useLastSliderButton = null!;
|
||||
|
||||
AddStep("enter editor", () => Game.ScreenStack.Push(new EditorLoader()));
|
||||
AddUntilStep("wait for editor load", () => editor?.ReadyForUse == true);
|
||||
AddStep("retrieve controls", () =>
|
||||
{
|
||||
var toolbox = this.ChildrenOfType<OsuSliderVelocityToolboxGroup>().Single();
|
||||
velocitySlider = toolbox.ChildrenOfType<ExpandableSlider<double>>().Single();
|
||||
useLastSliderButton = toolbox.ChildrenOfType<ExpandableButton>().Single();
|
||||
});
|
||||
|
||||
AddAssert("velocity slider at 1x", () => velocitySlider.Current.Value, () => Is.EqualTo(1));
|
||||
AddStep("expand right toolbox", () => InputManager.MoveMouseTo(this.ChildrenOfType<ExpandingToolboxContainer>().Last()));
|
||||
AddUntilStep("wait for expand", () => useLastSliderButton.Expanded.Value, () => Is.True);
|
||||
AddAssert("use last slider button disabled", () => useLastSliderButton.Enabled.Value, () => Is.False);
|
||||
|
||||
AddStep("seek to 5000", () => editorClock.Seek(5000));
|
||||
AddStep("set 2x velocity", () => velocitySlider.Current.Value = 2);
|
||||
placeSlider();
|
||||
AddAssert("placed slider has 2x velocity", () => editorBeatmap.HitObjects.OfType<Slider>().Last().SliderVelocityMultiplier, () => Is.EqualTo(2));
|
||||
AddStep("expand right toolbox", () => InputManager.MoveMouseTo(this.ChildrenOfType<ExpandingToolboxContainer>().Last()));
|
||||
AddUntilStep("wait for expand", () => useLastSliderButton.Expanded.Value, () => Is.True);
|
||||
AddAssert("use last slider button enabled", () => useLastSliderButton.Enabled.Value, () => Is.True);
|
||||
|
||||
AddStep("seek to 6000", () => editorClock.Seek(6000));
|
||||
placeSlider();
|
||||
AddAssert("placed slider has 2x velocity", () => editorBeatmap.HitObjects.OfType<Slider>().Last().SliderVelocityMultiplier, () => Is.EqualTo(2));
|
||||
AddStep("expand right toolbox", () => InputManager.MoveMouseTo(this.ChildrenOfType<ExpandingToolboxContainer>().Last()));
|
||||
AddUntilStep("wait for expand", () => useLastSliderButton.Expanded.Value, () => Is.True);
|
||||
AddAssert("use last slider button enabled", () => useLastSliderButton.Enabled.Value, () => Is.True);
|
||||
|
||||
AddStep("seek to 9000", () => editorClock.Seek(9000));
|
||||
AddStep("set 3x velocity", () => velocitySlider.Current.Value = 3);
|
||||
placeSlider();
|
||||
AddAssert("placed slider has 3x velocity", () => editorBeatmap.HitObjects.OfType<Slider>().Last().SliderVelocityMultiplier, () => Is.EqualTo(3));
|
||||
AddStep("expand right toolbox", () => InputManager.MoveMouseTo(this.ChildrenOfType<ExpandingToolboxContainer>().Last()));
|
||||
AddUntilStep("wait for expand", () => useLastSliderButton.Expanded.Value, () => Is.True);
|
||||
AddAssert("use last slider button enabled", () => useLastSliderButton.Enabled.Value, () => Is.True);
|
||||
|
||||
AddStep("seek to 10000", () => editorClock.Seek(10000));
|
||||
AddStep("set 1x velocity", () => velocitySlider.Current.Value = 1);
|
||||
AddStep("use last slider velocity instead", () => useLastSliderButton.TriggerClick());
|
||||
placeSlider();
|
||||
AddAssert("placed slider has 3x velocity", () => editorBeatmap.HitObjects.OfType<Slider>().Last().SliderVelocityMultiplier, () => Is.EqualTo(3));
|
||||
AddStep("expand right toolbox", () => InputManager.MoveMouseTo(this.ChildrenOfType<ExpandingToolboxContainer>().Last()));
|
||||
AddUntilStep("wait for expand", () => useLastSliderButton.Expanded.Value, () => Is.True);
|
||||
AddAssert("use last slider button disabled", () => useLastSliderButton.Enabled.Value, () => Is.False);
|
||||
|
||||
AddStep("seek back to 7000", () => editorClock.Seek(7000));
|
||||
placeSlider();
|
||||
AddAssert("placed slider has 2x velocity", () => editorBeatmap.HitObjects.OfType<Slider>().ElementAt(2).SliderVelocityMultiplier, () => Is.EqualTo(2));
|
||||
AddStep("expand right toolbox", () => InputManager.MoveMouseTo(this.ChildrenOfType<ExpandingToolboxContainer>().Last()));
|
||||
AddUntilStep("wait for expand", () => useLastSliderButton.Expanded.Value, () => Is.True);
|
||||
AddAssert("use last slider button disabled", () => useLastSliderButton.Enabled.Value, () => Is.False);
|
||||
|
||||
void placeSlider()
|
||||
{
|
||||
AddStep("enter slider placement mode", () => InputManager.Key(Key.Number3));
|
||||
AddStep("move mouse to top left", () => InputManager.MoveMouseTo(editor.ChildrenOfType<Playfield>().First().ScreenSpaceDrawQuad.TopLeft + new Vector2(50)));
|
||||
AddStep("start placement", () => InputManager.Click(MouseButton.Left));
|
||||
AddStep("move mouse to bottom right", () => InputManager.MoveMouseTo(editor.ChildrenOfType<Playfield>().First().ScreenSpaceDrawQuad.BottomRight - new Vector2(50)));
|
||||
AddStep("end placement", () => InputManager.Click(MouseButton.Right));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
// 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.Rulesets.Osu.Mods;
|
||||
using osu.Game.Tests.Rulesets;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
public class OsuScoreMultiplierTest : RulesetScoreMultiplierTest
|
||||
{
|
||||
public OsuScoreMultiplierTest()
|
||||
: base(new OsuRuleset())
|
||||
{
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFlashlightOnNonDefaultSettings()
|
||||
=> TestModCombination([new OsuModFlashlight { ComboBasedSize = { Value = false } }]);
|
||||
|
||||
[Test]
|
||||
public void TestHiddenOnNonDefaultSettings()
|
||||
=> TestModCombination([new OsuModHidden { OnlyFadeApproachCircles = { Value = true } }]);
|
||||
|
||||
[Test]
|
||||
public void TestHalfTimeSpeeds([Values(0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99)] double speedChange)
|
||||
=> TestModCombination([new OsuModHalfTime { SpeedChange = { Value = speedChange } }]);
|
||||
|
||||
[Test]
|
||||
public void TestDaycoreSpeeds([Values(0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99)] double speedChange)
|
||||
=> TestModCombination([new OsuModDaycore { SpeedChange = { Value = speedChange } }]);
|
||||
|
||||
[Test]
|
||||
public void TestDoubleTimeSpeeds([Values(1.01, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3, 1.35, 1.4, 1.45, 1.5, 1.55, 1.6, 1.65, 1.7, 1.75, 1.8, 1.85, 1.9, 1.95, 2)] double speedChange)
|
||||
=> TestModCombination([new OsuModDoubleTime { SpeedChange = { Value = speedChange } }]);
|
||||
|
||||
[Test]
|
||||
public void TestNightcoreSpeeds([Values(1.01, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3, 1.35, 1.4, 1.45, 1.5, 1.55, 1.6, 1.65, 1.7, 1.75, 1.8, 1.85, 1.9, 1.95, 2)] double speedChange)
|
||||
=> TestModCombination([new OsuModNightcore { SpeedChange = { Value = speedChange } }]);
|
||||
|
||||
[Test]
|
||||
public void TestMultiplicativeCombination()
|
||||
=> TestModCombination([new OsuModHidden(), new OsuModHardRock()]);
|
||||
}
|
||||
}
|
||||
@@ -792,7 +792,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
AddStep("export beatmap", () =>
|
||||
{
|
||||
var beatmapEncoder = new LegacyBeatmapEncoder(playableBeatmap, null);
|
||||
var beatmapEncoder = new LegacyBeatmapEncoder(playableBeatmap, null, null);
|
||||
|
||||
using (var stream = File.Open(Path.Combine(exportLocation, $"{testCaseName}.osu"), FileMode.Create))
|
||||
{
|
||||
|
||||
@@ -54,6 +54,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
[Resolved]
|
||||
private EditorClock? editorClock { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private OsuSliderVelocityToolboxGroup? sliderVelocityToolbox { get; set; }
|
||||
|
||||
private Bindable<bool> limitedDistanceSnap { get; set; } = null!;
|
||||
|
||||
private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder { Degree = 4 };
|
||||
@@ -111,9 +114,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
}
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private EditorBeatmap editorBeatmap { get; set; } = null!;
|
||||
|
||||
public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
|
||||
{
|
||||
var result = composer?.TrySnapToNearbyObjects(screenSpacePosition, fallbackTime);
|
||||
@@ -129,11 +129,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
case SliderPlacementState.Initial:
|
||||
BeginPlacement();
|
||||
|
||||
double? nearestSliderVelocity = (editorBeatmap
|
||||
.HitObjects
|
||||
.LastOrDefault(h => h is Slider && h.GetEndTime() < HitObject.StartTime) as Slider)?.SliderVelocityMultiplier;
|
||||
|
||||
HitObject.SliderVelocityMultiplier = nearestSliderVelocity ?? 1;
|
||||
HitObject.SliderVelocityMultiplier = sliderVelocityToolbox?.SliderVelocity.Value ?? 1;
|
||||
HitObject.Position = ToLocalSpace(result.ScreenSpacePosition);
|
||||
|
||||
// Replacing the DifficultyControlPoint above doesn't trigger any kind of invalidation.
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
// 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.Edit.Checks;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit.Checks
|
||||
{
|
||||
public class CheckOsuFewHitsounds : CheckFewHitsounds
|
||||
{
|
||||
protected override bool IsExcludedFromHitsounding(HitObject hitObject) => hitObject is Spinner;
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
public partial class FreehandSliderToolboxGroup : EditorToolboxGroup
|
||||
{
|
||||
public FreehandSliderToolboxGroup()
|
||||
: base("slider")
|
||||
: base("freehand")
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -7,14 +7,13 @@ using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Tools;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
public class HitCircleCompositionTool : CompositionTool
|
||||
{
|
||||
public HitCircleCompositionTool()
|
||||
: base(nameof(HitCircle))
|
||||
: base("Hit circle")
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,9 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
private readonly List<ICheck> checks = new List<ICheck>
|
||||
{
|
||||
// Audio
|
||||
new CheckOsuFewHitsounds(),
|
||||
|
||||
// Compose
|
||||
new CheckOffscreenObjects(),
|
||||
new CheckTooShortSpinners(),
|
||||
|
||||
@@ -125,13 +125,23 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
startPositionXSlider = new ExpandableSlider<float>
|
||||
{
|
||||
Current = StartPositionX,
|
||||
Current = new BindableFloat
|
||||
{
|
||||
MinValue = -OsuPlayfield.BASE_SIZE.X / 2,
|
||||
MaxValue = OsuPlayfield.BASE_SIZE.X / 2,
|
||||
Precision = 0.1f,
|
||||
},
|
||||
KeyboardStep = 1,
|
||||
ExpandedLabelText = "X offset",
|
||||
},
|
||||
startPositionYSlider = new ExpandableSlider<float>
|
||||
{
|
||||
Current = StartPositionY,
|
||||
Current = new BindableFloat
|
||||
{
|
||||
MinValue = -OsuPlayfield.BASE_SIZE.Y / 2,
|
||||
MaxValue = OsuPlayfield.BASE_SIZE.Y / 2,
|
||||
Precision = 0.1f,
|
||||
},
|
||||
KeyboardStep = 1,
|
||||
ExpandedLabelText = "Y offset",
|
||||
},
|
||||
@@ -186,15 +196,27 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
StartPositionX.BindValueChanged(x =>
|
||||
{
|
||||
startPositionXSlider.ContractedLabelText = $"X: {x.NewValue:#,0.##}";
|
||||
startPositionXSlider.Current.Value = x.NewValue - OsuPlayfield.BASE_SIZE.X / 2;
|
||||
StartPosition.Value = new Vector2(x.NewValue, StartPosition.Value.Y);
|
||||
}, true);
|
||||
|
||||
StartPositionY.BindValueChanged(y =>
|
||||
{
|
||||
startPositionYSlider.ContractedLabelText = $"Y: {y.NewValue:#,0.##}";
|
||||
startPositionYSlider.Current.Value = y.NewValue - OsuPlayfield.BASE_SIZE.Y / 2;
|
||||
StartPosition.Value = new Vector2(StartPosition.Value.X, y.NewValue);
|
||||
}, true);
|
||||
|
||||
startPositionXSlider.Current.BindValueChanged(x =>
|
||||
{
|
||||
StartPositionX.Value = x.NewValue + OsuPlayfield.BASE_SIZE.X / 2;
|
||||
});
|
||||
|
||||
startPositionYSlider.Current.BindValueChanged(y =>
|
||||
{
|
||||
StartPositionY.Value = y.NewValue + OsuPlayfield.BASE_SIZE.Y / 2;
|
||||
});
|
||||
|
||||
StartPosition.BindValueChanged(pos =>
|
||||
{
|
||||
StartPositionX.Value = pos.NewValue.X;
|
||||
|
||||
@@ -75,6 +75,9 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
[Cached(typeof(IDistanceSnapProvider))]
|
||||
public readonly OsuDistanceSnapProvider DistanceSnapProvider = new OsuDistanceSnapProvider();
|
||||
|
||||
[Cached]
|
||||
private readonly OsuSliderVelocityToolboxGroup sliderVelocityToolboxGroup = new OsuSliderVelocityToolboxGroup();
|
||||
|
||||
[Cached]
|
||||
protected readonly OsuGridToolboxGroup OsuGridToolboxGroup = new OsuGridToolboxGroup();
|
||||
|
||||
@@ -111,6 +114,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
RightToolbox.AddRange(new Drawable[]
|
||||
{
|
||||
sliderVelocityToolboxGroup,
|
||||
OsuGridToolboxGroup,
|
||||
new TransformToolboxGroup
|
||||
{
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Caching;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
public partial class OsuSliderVelocityToolboxGroup : EditorToolboxGroup
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the last slider's velocity should be used (if available).
|
||||
/// </summary>
|
||||
private bool useLastSliderVelocity;
|
||||
|
||||
/// <summary>
|
||||
/// The slider velocity to be used for new object placements.
|
||||
/// </summary>
|
||||
public IBindable<double> SliderVelocity => sliderVelocity;
|
||||
|
||||
private readonly BindableDouble sliderVelocity = new BindableDouble(1)
|
||||
{
|
||||
Precision = 0.01,
|
||||
MinValue = 0.1,
|
||||
MaxValue = 10,
|
||||
};
|
||||
|
||||
private ExpandableSlider<double> slider = null!;
|
||||
private ExpandableButton useLastSliderButton = null!;
|
||||
|
||||
[Resolved]
|
||||
private EditorBeatmap editorBeatmap { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private EditorClock editorClock { get; set; } = null!;
|
||||
|
||||
private bool syncingBindables;
|
||||
private double lastClockPosition = double.NegativeInfinity;
|
||||
private readonly Cached<Slider?> sliderVelocitySourceObject = new Cached<Slider?>();
|
||||
|
||||
public OsuSliderVelocityToolboxGroup()
|
||||
: base("velocity")
|
||||
{
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Spacing = new Vector2(5);
|
||||
Children = new Drawable[]
|
||||
{
|
||||
slider = new ExpandableSlider<double>
|
||||
{
|
||||
ExpandedLabelText = "Slider velocity",
|
||||
Current = new BindableDouble(1)
|
||||
{
|
||||
Precision = 0.01,
|
||||
MinValue = 0.1,
|
||||
MaxValue = 10,
|
||||
},
|
||||
KeyboardStep = 0.1f,
|
||||
},
|
||||
useLastSliderButton = new ExpandableButton
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Action = () =>
|
||||
{
|
||||
useLastSliderVelocity = true;
|
||||
sliderVelocitySourceObject.Invalidate();
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
// set unconditionally to true initially.
|
||||
// if there is no object available to get the slider velocity from, the code in `Update()` will handle that.
|
||||
useLastSliderVelocity = true;
|
||||
|
||||
sliderVelocity.BindValueChanged(_ => updateSliderFromVelocity(), true);
|
||||
slider.Current.BindValueChanged(_ =>
|
||||
{
|
||||
updateVelocityFromSlider();
|
||||
updateContractedText();
|
||||
});
|
||||
updateContractedText();
|
||||
useLastSliderButton.Expanded.BindValueChanged(_ => sliderVelocitySourceObject.Invalidate());
|
||||
|
||||
editorBeatmap.HitObjectAdded += invalidateSliderVelocitySourceObject;
|
||||
editorBeatmap.HitObjectUpdated += invalidateSliderVelocitySourceObject;
|
||||
editorBeatmap.HitObjectRemoved += invalidateSliderVelocitySourceObject;
|
||||
}
|
||||
|
||||
private void updateContractedText()
|
||||
{
|
||||
slider.ContractedLabelText = LocalisableString.Interpolate($@"SV: {slider.Current.Value.ToLocalisableString("N2")}x");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the displayed value of this toolbox's slider from a change to <see cref="SliderVelocity"/>
|
||||
/// (which is the source-of-truth used for new object placements).
|
||||
/// This is only relevant when <see cref="useLastSliderVelocity"/> is true,
|
||||
/// in which case this code is responsible for propagating the velocity from <see cref="sliderVelocitySourceObject"/> to the slider.
|
||||
/// </summary>
|
||||
private void updateSliderFromVelocity()
|
||||
{
|
||||
if (syncingBindables)
|
||||
return;
|
||||
|
||||
if (!useLastSliderVelocity)
|
||||
return;
|
||||
|
||||
syncingBindables = true;
|
||||
slider.Current.Value = sliderVelocity.Value;
|
||||
syncingBindables = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the value of <see cref="SliderVelocity"/> from a change to the slider's state.
|
||||
/// This change is assumed to be user-provoked, and therefore <see cref="useLastSliderVelocity"/> is switched unconditionally off
|
||||
/// as the presumed intent is to override the velocity from <see cref="sliderVelocitySourceObject"/>.
|
||||
/// </summary>
|
||||
private void updateVelocityFromSlider()
|
||||
{
|
||||
if (syncingBindables)
|
||||
return;
|
||||
|
||||
syncingBindables = true;
|
||||
useLastSliderVelocity = false;
|
||||
sliderVelocity.Value = slider.Current.Value;
|
||||
syncingBindables = false;
|
||||
sliderVelocitySourceObject.Invalidate();
|
||||
}
|
||||
|
||||
private void invalidateSliderVelocitySourceObject(HitObject _) => sliderVelocitySourceObject.Invalidate();
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (editorClock.CurrentTime != lastClockPosition)
|
||||
{
|
||||
sliderVelocitySourceObject.Invalidate();
|
||||
lastClockPosition = editorClock.CurrentTime;
|
||||
}
|
||||
|
||||
// Three possible causes of invalidation:
|
||||
// - The user seeked the clock, which means a different velocity source object needs to be used.
|
||||
// - Some change to the beatmap was made, which means the previously-used velocity source object may no longer be the most relevant one.
|
||||
// - The user is interacting with the toolbox in a way that requires a visual state update
|
||||
// (hovered to expand it, clicked the button to use last slider's velocity, or dragged the manual velocity slider).
|
||||
// This is a procedural one, because `sliderVelocitySourceObject` will have been pointing at the correct object already,
|
||||
// but to decrease unnecessary work being done every frame, the invalidation is explicitly re-triggered to update the toolbox state.
|
||||
if (!sliderVelocitySourceObject.IsValid)
|
||||
{
|
||||
var lastSlider = getLastSlider();
|
||||
sliderVelocitySourceObject.Value = lastSlider;
|
||||
|
||||
if (lastSlider == null)
|
||||
{
|
||||
useLastSliderButton.Enabled.Value = false;
|
||||
useLastSliderButton.ExpandedLabelText = "No sliders to get velocity from";
|
||||
useLastSliderButton.ContractedLabelText = default;
|
||||
}
|
||||
else
|
||||
{
|
||||
useLastSliderButton.Enabled.Value = useLastSliderButton.Expanded.Value && !useLastSliderVelocity;
|
||||
useLastSliderButton.ExpandedLabelText = useLastSliderVelocity
|
||||
? "Using last slider's velocity"
|
||||
: LocalisableString.Interpolate($@"Use last slider's velocity ({lastSlider.SliderVelocityMultiplier.ToLocalisableString("N2")}x)");
|
||||
useLastSliderButton.ContractedLabelText = $@"current {lastSlider.SliderVelocityMultiplier.ToLocalisableString("N2")}x";
|
||||
if (useLastSliderVelocity)
|
||||
sliderVelocity.Value = lastSlider.SliderVelocityMultiplier;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Slider? getLastSlider()
|
||||
{
|
||||
return editorBeatmap
|
||||
.HitObjects
|
||||
.OfType<Slider>()
|
||||
.LastOrDefault(h => h.StartTime <= editorClock.CurrentTime);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
if (editorBeatmap.IsNotNull())
|
||||
{
|
||||
editorBeatmap.HitObjectAdded -= invalidateSliderVelocitySourceObject;
|
||||
editorBeatmap.HitObjectUpdated -= invalidateSliderVelocitySourceObject;
|
||||
editorBeatmap.HitObjectRemoved -= invalidateSliderVelocitySourceObject;
|
||||
}
|
||||
|
||||
base.Dispose(isDisposing);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public partial class OsuModBlinds : Mod, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToHealthProcessor
|
||||
public partial class OsuModBlinds : ModBlinds, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToHealthProcessor
|
||||
{
|
||||
public override string Name => "Blinds";
|
||||
public override LocalisableString Description => "Play with blinds on your screen.";
|
||||
|
||||
@@ -16,7 +16,7 @@ using osu.Game.Rulesets.Osu.Skinning.Default;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModTraceable : ModWithVisibilityAdjustment, IRequiresApproachCircles
|
||||
public class OsuModTraceable : ModTraceable, IRequiresApproachCircles
|
||||
{
|
||||
public override string Name => "Traceable";
|
||||
public override string Acronym => "TC";
|
||||
|
||||
@@ -234,6 +234,8 @@ namespace osu.Game.Rulesets.Osu
|
||||
}
|
||||
}
|
||||
|
||||
public override ScoreMultiplierCalculator CreateScoreMultiplierCalculator() => new OsuScoreMultiplierCalculator();
|
||||
|
||||
public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.RulesetOsu };
|
||||
|
||||
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new OsuDifficultyCalculator(RulesetInfo, beatmap);
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
// 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.Mods;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Scoring
|
||||
{
|
||||
public class OsuScoreMultiplierCalculator : ScoreMultiplierCalculator
|
||||
{
|
||||
static OsuScoreMultiplierCalculator()
|
||||
{
|
||||
#region Difficulty Reduction
|
||||
|
||||
Single<OsuModEasy>(hasMultiplier: 0.5);
|
||||
Single<OsuModNoFail>(hasMultiplier: 0.5);
|
||||
Single<OsuModHalfTime>(hasMultiplier: halfTime => rateAdjustMultiplier(halfTime.SpeedChange.Value));
|
||||
Single<OsuModDaycore>(hasMultiplier: daycore => rateAdjustMultiplier(daycore.SpeedChange.Value));
|
||||
|
||||
#endregion
|
||||
|
||||
#region Difficulty Increase
|
||||
|
||||
Single<OsuModHardRock>(hasMultiplier: hardRock => hardRock.UsesDefaultConfiguration ? 1.06 : 1);
|
||||
// Sudden Death
|
||||
// Perfect
|
||||
Single<OsuModDoubleTime>(hasMultiplier: doubleTime => rateAdjustMultiplier(doubleTime.SpeedChange.Value));
|
||||
Single<OsuModNightcore>(hasMultiplier: nightcore => rateAdjustMultiplier(nightcore.SpeedChange.Value));
|
||||
Single<OsuModHidden>(hasMultiplier: hidden => hidden.UsesDefaultConfiguration ? 1.06 : 1);
|
||||
// Traceable
|
||||
Single<OsuModFlashlight>(hasMultiplier: flashlight => flashlight.UsesDefaultConfiguration ? 1.12 : 1);
|
||||
Single<OsuModBlinds>(hasMultiplier: blinds => blinds.UsesDefaultConfiguration ? 1.12 : 1);
|
||||
// Strict Tracking
|
||||
// Accuracy Challenge
|
||||
|
||||
#endregion
|
||||
|
||||
#region Conversion
|
||||
|
||||
Single<OsuModTargetPractice>(hasMultiplier: 0.1);
|
||||
Single<OsuModDifficultyAdjust>(hasMultiplier: 0.5);
|
||||
Single<OsuModClassic>(hasMultiplier: 0.96);
|
||||
// Random
|
||||
// Mirror
|
||||
// Alternate
|
||||
// Single Tap
|
||||
|
||||
#endregion
|
||||
|
||||
#region Automation
|
||||
|
||||
// Autoplay
|
||||
// Cinema
|
||||
Single<OsuModRelax>(hasMultiplier: 0.1);
|
||||
Single<OsuModAutopilot>(hasMultiplier: 0.1);
|
||||
Single<OsuModSpunOut>(hasMultiplier: 0.9);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Fun
|
||||
|
||||
// Transform
|
||||
// Wiggle
|
||||
// Spin In
|
||||
// Grow
|
||||
// Deflate
|
||||
Single<ModWindUp>(hasMultiplier: 0.5);
|
||||
Single<ModWindDown>(hasMultiplier: 0.5);
|
||||
// Barrel Roll
|
||||
// Approach Different
|
||||
// Muted
|
||||
// No Scope
|
||||
Single<OsuModMagnetised>(hasMultiplier: 0.5);
|
||||
// Repel
|
||||
Single<ModAdaptiveSpeed>(hasMultiplier: 0.5);
|
||||
// Freeze Frame
|
||||
// Bubbles
|
||||
Single<OsuModSynesthesia>(hasMultiplier: 0.8);
|
||||
// Depth
|
||||
// Bloom
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
private static double rateAdjustMultiplier(double speedChange)
|
||||
{
|
||||
// Round to the nearest multiple of 0.1.
|
||||
double value = (int)(speedChange * 10) / 10.0;
|
||||
|
||||
// Offset back to 0.
|
||||
value -= 1;
|
||||
|
||||
if (speedChange >= 1)
|
||||
return 1 + value / 5;
|
||||
else
|
||||
return 0.6 + value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.Rulesets.Taiko.Mods;
|
||||
using osu.Game.Tests.Rulesets;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Tests
|
||||
{
|
||||
public class TaikoScoreMultiplierTest : RulesetScoreMultiplierTest
|
||||
{
|
||||
public TaikoScoreMultiplierTest()
|
||||
: base(new TaikoRuleset())
|
||||
{
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFlashlightOnNonDefaultSettings()
|
||||
=> TestModCombination([new TaikoModFlashlight { ComboBasedSize = { Value = false } }]);
|
||||
|
||||
[Test]
|
||||
public void TestHalfTimeSpeeds([Values(0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99)] double speedChange)
|
||||
=> TestModCombination([new TaikoModHalfTime { SpeedChange = { Value = speedChange } }]);
|
||||
|
||||
[Test]
|
||||
public void TestDaycoreSpeeds([Values(0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99)] double speedChange)
|
||||
=> TestModCombination([new TaikoModDaycore { SpeedChange = { Value = speedChange } }]);
|
||||
|
||||
[Test]
|
||||
public void TestDoubleTimeSpeeds([Values(1.01, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3, 1.35, 1.4, 1.45, 1.5, 1.55, 1.6, 1.65, 1.7, 1.75, 1.8, 1.85, 1.9, 1.95, 2)] double speedChange)
|
||||
=> TestModCombination([new TaikoModDoubleTime { SpeedChange = { Value = speedChange } }]);
|
||||
|
||||
[Test]
|
||||
public void TestNightcoreSpeeds([Values(1.01, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3, 1.35, 1.4, 1.45, 1.5, 1.55, 1.6, 1.65, 1.7, 1.75, 1.8, 1.85, 1.9, 1.95, 2)] double speedChange)
|
||||
=> TestModCombination([new TaikoModNightcore { SpeedChange = { Value = speedChange } }]);
|
||||
|
||||
[Test]
|
||||
public void TestMultiplicativeCombination()
|
||||
=> TestModCombination([new TaikoModHidden(), new TaikoModHardRock()]);
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
currentStoryboard = new Storyboard();
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
currentStoryboard.GetLayer("Foreground").Add(new StoryboardSprite($"test{i}", Anchor.Centre, Vector2.Zero));
|
||||
currentStoryboard.GetLayer("Foreground").Add(new StoryboardSprite(StoryboardElementSource.Beatmap, $"test{i}", Anchor.Centre, Vector2.Zero));
|
||||
});
|
||||
|
||||
CreateTest();
|
||||
@@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
currentStoryboard = new Storyboard();
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
currentStoryboard.GetLayer("Overlay").Add(new StoryboardSprite($"test{i}", Anchor.Centre, Vector2.Zero));
|
||||
currentStoryboard.GetLayer("Overlay").Add(new StoryboardSprite(StoryboardElementSource.Beatmap, $"test{i}", Anchor.Centre, Vector2.Zero));
|
||||
});
|
||||
|
||||
CreateTest();
|
||||
|
||||
@@ -18,11 +18,13 @@ namespace osu.Game.Rulesets.Taiko.Configuration
|
||||
base.InitialiseDefaults();
|
||||
|
||||
SetDefault(TaikoRulesetSetting.TouchControlScheme, TaikoTouchControlScheme.KDDK);
|
||||
SetDefault(TaikoRulesetSetting.RateAdjustedHitAnimation, true);
|
||||
}
|
||||
}
|
||||
|
||||
public enum TaikoRulesetSetting
|
||||
{
|
||||
TouchControlScheme
|
||||
TouchControlScheme,
|
||||
RateAdjustedHitAnimation,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,22 +2,22 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Tools;
|
||||
using osu.Game.Rulesets.Taiko.Edit.Blueprints;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Edit
|
||||
{
|
||||
public class DrumRollCompositionTool : CompositionTool
|
||||
{
|
||||
public DrumRollCompositionTool()
|
||||
: base(nameof(DrumRoll))
|
||||
: base("Drum roll")
|
||||
{
|
||||
}
|
||||
|
||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders);
|
||||
public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.EditorDrumRoll };
|
||||
|
||||
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new DrumRollPlacementBlueprint();
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Tools;
|
||||
using osu.Game.Rulesets.Taiko.Edit.Blueprints;
|
||||
@@ -17,7 +18,7 @@ namespace osu.Game.Rulesets.Taiko.Edit
|
||||
{
|
||||
}
|
||||
|
||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles);
|
||||
public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.EditorHit };
|
||||
|
||||
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new HitPlacementBlueprint();
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Tools;
|
||||
using osu.Game.Rulesets.Taiko.Edit.Blueprints;
|
||||
@@ -17,7 +18,7 @@ namespace osu.Game.Rulesets.Taiko.Edit
|
||||
{
|
||||
}
|
||||
|
||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners);
|
||||
public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.EditorSwell };
|
||||
|
||||
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new SwellPlacementBlueprint();
|
||||
}
|
||||
|
||||
@@ -7,12 +7,15 @@ using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Taiko.Configuration;
|
||||
using osu.Game.Rulesets.Taiko.Skinning.Default;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
@@ -34,12 +37,15 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
private set;
|
||||
}
|
||||
|
||||
private bool validActionPressed;
|
||||
|
||||
private double? lastPressHandleTime;
|
||||
[Resolved(CanBeNull = true)]
|
||||
private TaikoRulesetConfigManager taikoConfig { get; set; }
|
||||
|
||||
private readonly Bindable<bool> rateAdjustedHitAnimations = new Bindable<bool>(true);
|
||||
private readonly Bindable<HitType> type = new Bindable<HitType>();
|
||||
|
||||
private bool validActionPressed;
|
||||
private double? lastPressHandleTime;
|
||||
|
||||
public DrawableHit()
|
||||
: this(null)
|
||||
{
|
||||
@@ -51,6 +57,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
FillMode = FillMode.Fit;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
taikoConfig?.BindWith(TaikoRulesetSetting.RateAdjustedHitAnimation, rateAdjustedHitAnimations);
|
||||
}
|
||||
|
||||
protected override void OnApply()
|
||||
{
|
||||
type.BindTo(HitObject.TypeBindable);
|
||||
@@ -168,11 +180,15 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
if (SnapJudgementLocation)
|
||||
MainPiece.MoveToX(-X);
|
||||
|
||||
this.ScaleTo(0.8f, gravity_time * 2, Easing.OutQuad);
|
||||
// Rate independent to match stable.
|
||||
double rate = Math.Abs((Clock as IGameplayClock)?.GetTrueGameplayRate() ?? Clock.Rate);
|
||||
double length = gravity_time * (rateAdjustedHitAnimations.Value ? 1 : rate);
|
||||
|
||||
this.MoveToY(-gravity_travel_height, gravity_time, Easing.Out)
|
||||
this.ScaleTo(0.8f, length * 2, Easing.OutQuad);
|
||||
|
||||
this.MoveToY(-gravity_travel_height, length, Easing.Out)
|
||||
.Then()
|
||||
.MoveToY(gravity_travel_height * 2, gravity_time * 2, Easing.In);
|
||||
.MoveToY(gravity_travel_height * 2, length * 2, Easing.In);
|
||||
|
||||
this.FadeOut(800);
|
||||
break;
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
// 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.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Taiko.Mods;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Scoring
|
||||
{
|
||||
public class TaikoScoreMultiplierCalculator : ScoreMultiplierCalculator
|
||||
{
|
||||
static TaikoScoreMultiplierCalculator()
|
||||
{
|
||||
#region Difficulty Reduction
|
||||
|
||||
Single<TaikoModEasy>(hasMultiplier: 0.5);
|
||||
Single<TaikoModNoFail>(hasMultiplier: 0.5);
|
||||
Single<TaikoModHalfTime>(hasMultiplier: halfTime => rateAdjustMultiplier(halfTime.SpeedChange.Value));
|
||||
Single<TaikoModDaycore>(hasMultiplier: daycore => rateAdjustMultiplier(daycore.SpeedChange.Value));
|
||||
Single<TaikoModSimplifiedRhythm>(hasMultiplier: 0.6);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Difficulty Increase
|
||||
|
||||
Single<TaikoModHardRock>(hasMultiplier: hardRock => hardRock.UsesDefaultConfiguration ? 1.06 : 1);
|
||||
// Sudden Death
|
||||
// Perfect
|
||||
Single<TaikoModDoubleTime>(hasMultiplier: doubleTime => rateAdjustMultiplier(doubleTime.SpeedChange.Value));
|
||||
Single<TaikoModNightcore>(hasMultiplier: nightcore => rateAdjustMultiplier(nightcore.SpeedChange.Value));
|
||||
Single<TaikoModHidden>(hasMultiplier: hidden => hidden.UsesDefaultConfiguration ? 1.06 : 1);
|
||||
Single<TaikoModFlashlight>(hasMultiplier: flashlight => flashlight.UsesDefaultConfiguration ? 1.12 : 1);
|
||||
// Accuracy Challenge
|
||||
|
||||
#endregion
|
||||
|
||||
#region Conversion
|
||||
|
||||
// Random
|
||||
Single<TaikoModDifficultyAdjust>(hasMultiplier: 0.5);
|
||||
Single<TaikoModClassic>(hasMultiplier: 0.96);
|
||||
// Swap
|
||||
// Single Tap
|
||||
Single<TaikoModConstantSpeed>(hasMultiplier: 0.9);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Automation
|
||||
|
||||
// Autoplay
|
||||
// Cinema
|
||||
Single<TaikoModRelax>(hasMultiplier: 0.1);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Fun
|
||||
|
||||
Single<ModWindUp>(hasMultiplier: 0.5);
|
||||
Single<ModWindDown>(hasMultiplier: 0.5);
|
||||
// Muted
|
||||
Single<ModAdaptiveSpeed>(hasMultiplier: 0.5);
|
||||
|
||||
#endregion
|
||||
|
||||
#region System
|
||||
|
||||
// Score V2
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
private static double rateAdjustMultiplier(double speedChange)
|
||||
{
|
||||
// Round to the nearest multiple of 0.1.
|
||||
double value = (int)(speedChange * 10) / 10.0;
|
||||
|
||||
// Offset back to 0.
|
||||
value -= 1;
|
||||
|
||||
if (speedChange >= 1)
|
||||
return 1 + value / 5;
|
||||
else
|
||||
return 0.6 + value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -188,6 +188,8 @@ namespace osu.Game.Rulesets.Taiko
|
||||
}
|
||||
}
|
||||
|
||||
public override ScoreMultiplierCalculator CreateScoreMultiplierCalculator() => new TaikoScoreMultiplierCalculator();
|
||||
|
||||
public override string Description => "osu!taiko";
|
||||
|
||||
public override string ShortName => SHORT_NAME;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Localisation;
|
||||
@@ -31,7 +32,16 @@ namespace osu.Game.Rulesets.Taiko
|
||||
{
|
||||
Caption = RulesetSettingsStrings.TouchControlScheme,
|
||||
Current = config.GetBindable<TaikoTouchControlScheme>(TaikoRulesetSetting.TouchControlScheme)
|
||||
}),
|
||||
new SettingsItemV2(new FormCheckBox
|
||||
{
|
||||
Caption = RulesetSettingsStrings.RateAdjustedHitAnimation,
|
||||
HintText = RulesetSettingsStrings.RateAdjustedHitAnimationTooltip,
|
||||
Current = config.GetBindable<bool>(TaikoRulesetSetting.RateAdjustedHitAnimation)
|
||||
})
|
||||
{
|
||||
ApplyClassicDefault = c => ((IHasCurrentValue<bool>)c).Current.Value = false,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,14 +41,14 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
|
||||
private static IEnumerable<string> allBeatmaps = beatmaps_resource_store.GetAvailableResources().Where(res => res.EndsWith(".osu", StringComparison.Ordinal));
|
||||
|
||||
public record BeatmapComponents(IBeatmap Beatmap, LegacySkin Skin, Storyboard Storyboard);
|
||||
|
||||
[Test]
|
||||
public void TestUnsupportedStoryboardEvents()
|
||||
public void TestStoryboardEvents()
|
||||
{
|
||||
const string name = "Resources/storyboard_only_video.osu";
|
||||
|
||||
var decoded = DecodeFromLegacy(beatmaps_resource_store.GetStream(name), beatmaps_resource_store, name);
|
||||
Assert.That(decoded.beatmap.UnhandledEventLines.Count, Is.EqualTo(1));
|
||||
Assert.That(decoded.beatmap.UnhandledEventLines.Single(), Is.EqualTo("Video,0,\"video.avi\""));
|
||||
|
||||
var memoryStream = EncodeToLegacy(decoded);
|
||||
|
||||
@@ -63,8 +63,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
var decoded = DecodeFromLegacy(beatmaps_resource_store.GetStream(name), beatmaps_resource_store, name);
|
||||
var decodedAfterEncode = DecodeFromLegacy(EncodeToLegacy(decoded), beatmaps_resource_store, name);
|
||||
|
||||
Sort(decoded.beatmap);
|
||||
Sort(decodedAfterEncode.beatmap);
|
||||
Sort(decoded.Beatmap);
|
||||
Sort(decodedAfterEncode.Beatmap);
|
||||
|
||||
CompareBeatmaps(decoded, decodedAfterEncode);
|
||||
}
|
||||
@@ -76,10 +76,10 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
var decodedAfterEncode = DecodeFromLegacy(EncodeToLegacy(decoded), beatmaps_resource_store, name);
|
||||
|
||||
// run an extra convert. this is expected to be stable.
|
||||
decodedAfterEncode.beatmap = convert(decodedAfterEncode.beatmap);
|
||||
decodedAfterEncode = decodedAfterEncode with { Beatmap = convert(decodedAfterEncode.Beatmap) };
|
||||
|
||||
Sort(decoded.beatmap);
|
||||
Sort(decodedAfterEncode.beatmap);
|
||||
Sort(decoded.Beatmap);
|
||||
Sort(decodedAfterEncode.Beatmap);
|
||||
|
||||
CompareBeatmaps(decoded, decodedAfterEncode);
|
||||
}
|
||||
@@ -91,7 +91,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
|
||||
// we are testing that the transfer of relevant data to hitobjects (from legacy control points) sticks through encode/decode.
|
||||
// before the encode step, the legacy information is removed here.
|
||||
decoded.beatmap.ControlPointInfo = removeLegacyControlPointTypes(decoded.beatmap.ControlPointInfo);
|
||||
decoded.Beatmap.ControlPointInfo = removeLegacyControlPointTypes(decoded.Beatmap.ControlPointInfo);
|
||||
|
||||
var decodedAfterEncode = DecodeFromLegacy(EncodeToLegacy(decoded), beatmaps_resource_store, name);
|
||||
|
||||
@@ -120,17 +120,21 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
}
|
||||
}
|
||||
|
||||
public static void CompareBeatmaps((IBeatmap beatmap, TestLegacySkin skin) expected, (IBeatmap beatmap, TestLegacySkin skin) actual)
|
||||
public static void CompareBeatmaps(BeatmapComponents expected, BeatmapComponents actual)
|
||||
{
|
||||
// Check all control points that are still considered to be at a global level.
|
||||
Assert.That(actual.beatmap.ControlPointInfo.TimingPoints.Serialize(), Is.EqualTo(expected.beatmap.ControlPointInfo.TimingPoints.Serialize()));
|
||||
Assert.That(actual.beatmap.ControlPointInfo.EffectPoints.Serialize(), Is.EqualTo(expected.beatmap.ControlPointInfo.EffectPoints.Serialize()));
|
||||
Assert.That(actual.Beatmap.ControlPointInfo.TimingPoints.Serialize(), Is.EqualTo(expected.Beatmap.ControlPointInfo.TimingPoints.Serialize()));
|
||||
Assert.That(actual.Beatmap.ControlPointInfo.EffectPoints.Serialize(), Is.EqualTo(expected.Beatmap.ControlPointInfo.EffectPoints.Serialize()));
|
||||
|
||||
// Check all hitobjects.
|
||||
Assert.That(actual.beatmap.HitObjects.Serialize(), Is.EqualTo(expected.beatmap.HitObjects.Serialize()));
|
||||
Assert.That(actual.Beatmap.HitObjects.Serialize(), Is.EqualTo(expected.Beatmap.HitObjects.Serialize()));
|
||||
|
||||
// Check skin.
|
||||
ClassicAssert.True(areComboColoursEqual(expected.skin.Configuration, actual.skin.Configuration));
|
||||
ClassicAssert.True(areComboColoursEqual(expected.Skin.Configuration, actual.Skin.Configuration));
|
||||
|
||||
// Do a rough pass on storyboard layers.
|
||||
foreach (string layer in actual.Storyboard.Layers.Concat(expected.Storyboard.Layers).Select(l => l.Name).Distinct())
|
||||
Assert.That(actual.Storyboard.GetLayer(layer).Elements.Count, Is.EqualTo(expected.Storyboard.GetLayer(layer).Elements.Count));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -153,9 +157,9 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
}
|
||||
};
|
||||
|
||||
var encoded = EncodeToLegacy((beatmap, new TestLegacySkin(beatmaps_resource_store, string.Empty)));
|
||||
var encoded = EncodeToLegacy(new BeatmapComponents(beatmap, new TestLegacySkin(beatmaps_resource_store, string.Empty), new Storyboard()));
|
||||
var decodedAfterEncode = DecodeFromLegacy(encoded, beatmaps_resource_store, string.Empty);
|
||||
var decodedSlider = (Slider)decodedAfterEncode.beatmap.HitObjects[0];
|
||||
var decodedSlider = (Slider)decodedAfterEncode.Beatmap.HitObjects[0];
|
||||
Assert.That(decodedSlider.Path.ControlPoints.Count, Is.EqualTo(4));
|
||||
Assert.That(decodedSlider.Path.ControlPoints[0].Type, Is.EqualTo(PathType.BSpline(3)));
|
||||
Assert.That(decodedSlider.Path.ControlPoints[2].Type, Is.EqualTo(PathType.BSpline(3)));
|
||||
@@ -183,9 +187,9 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
}
|
||||
};
|
||||
|
||||
var encoded = EncodeToLegacy((beatmap, new TestLegacySkin(beatmaps_resource_store, string.Empty)));
|
||||
var encoded = EncodeToLegacy(new BeatmapComponents(beatmap, new TestLegacySkin(beatmaps_resource_store, string.Empty), new Storyboard()));
|
||||
var decodedAfterEncode = DecodeFromLegacy(encoded, beatmaps_resource_store, string.Empty);
|
||||
var decodedSlider = (Slider)decodedAfterEncode.beatmap.HitObjects[0];
|
||||
var decodedSlider = (Slider)decodedAfterEncode.Beatmap.HitObjects[0];
|
||||
Assert.That(decodedSlider.Path.ControlPoints.Count, Is.EqualTo(5));
|
||||
}
|
||||
|
||||
@@ -211,9 +215,9 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
}
|
||||
};
|
||||
|
||||
var encoded = EncodeToLegacy((new Beatmap(), beatmapSkin));
|
||||
var encoded = EncodeToLegacy(new BeatmapComponents(new Beatmap(), beatmapSkin, new Storyboard()));
|
||||
var decodedAfterEncode = DecodeFromLegacy(encoded, beatmaps_resource_store, string.Empty);
|
||||
Assert.That(decodedAfterEncode.skin.Configuration.CustomComboColours, Has.Count.EqualTo(8));
|
||||
Assert.That(decodedAfterEncode.Skin.Configuration.CustomComboColours, Has.Count.EqualTo(8));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -234,9 +238,9 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
HitObjects = { originalSlider }
|
||||
};
|
||||
|
||||
var encoded = EncodeToLegacy((beatmap, new TestLegacySkin(beatmaps_resource_store, string.Empty)));
|
||||
var encoded = EncodeToLegacy(new BeatmapComponents(beatmap, new TestLegacySkin(beatmaps_resource_store, string.Empty), new Storyboard()));
|
||||
var decodedAfterEncode = DecodeFromLegacy(encoded, beatmaps_resource_store, string.Empty, version: LegacyBeatmapEncoder.FIRST_LAZER_VERSION);
|
||||
var decodedSlider = (Slider)decodedAfterEncode.beatmap.HitObjects[0];
|
||||
var decodedSlider = (Slider)decodedAfterEncode.Beatmap.HitObjects[0];
|
||||
Assert.That(decodedSlider.Path.ControlPoints.Select(p => p.Position),
|
||||
Is.EquivalentTo(originalSlider.Path.ControlPoints.Select(p => p.Position)));
|
||||
}
|
||||
@@ -254,17 +258,17 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
}
|
||||
};
|
||||
|
||||
var encoded = EncodeToLegacy((beatmap, new TestLegacySkin(beatmaps_resource_store, string.Empty)));
|
||||
var encoded = EncodeToLegacy(new BeatmapComponents(beatmap, new TestLegacySkin(beatmaps_resource_store, string.Empty), new Storyboard()));
|
||||
var decodedAfterEncode = DecodeFromLegacy(encoded, beatmaps_resource_store, string.Empty);
|
||||
|
||||
Assert.That(decodedAfterEncode.beatmap.HitObjects[0].Samples[0].Suffix, Is.Null);
|
||||
Assert.That(decodedAfterEncode.beatmap.HitObjects[0].Samples[0].UseBeatmapSamples, Is.False);
|
||||
Assert.That(decodedAfterEncode.Beatmap.HitObjects[0].Samples[0].Suffix, Is.Null);
|
||||
Assert.That(decodedAfterEncode.Beatmap.HitObjects[0].Samples[0].UseBeatmapSamples, Is.False);
|
||||
|
||||
Assert.That(decodedAfterEncode.beatmap.HitObjects[1].Samples[0].Suffix, Is.Null);
|
||||
Assert.That(decodedAfterEncode.beatmap.HitObjects[1].Samples[0].UseBeatmapSamples, Is.True);
|
||||
Assert.That(decodedAfterEncode.Beatmap.HitObjects[1].Samples[0].Suffix, Is.Null);
|
||||
Assert.That(decodedAfterEncode.Beatmap.HitObjects[1].Samples[0].UseBeatmapSamples, Is.True);
|
||||
|
||||
Assert.That(decodedAfterEncode.beatmap.HitObjects[2].Samples[0].Suffix, Is.EqualTo("3"));
|
||||
Assert.That(decodedAfterEncode.beatmap.HitObjects[2].Samples[0].UseBeatmapSamples, Is.True);
|
||||
Assert.That(decodedAfterEncode.Beatmap.HitObjects[2].Samples[0].Suffix, Is.EqualTo("3"));
|
||||
Assert.That(decodedAfterEncode.Beatmap.HitObjects[2].Samples[0].UseBeatmapSamples, Is.True);
|
||||
}
|
||||
|
||||
private static bool areComboColoursEqual(IHasComboColours a, IHasComboColours b)
|
||||
@@ -289,7 +293,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
}
|
||||
}
|
||||
|
||||
public static (IBeatmap beatmap, TestLegacySkin skin) DecodeFromLegacy(Stream stream, IResourceStore<byte[]> beatmapsResourceStore, string name, int version = LegacyDecoder<Beatmap>.LATEST_VERSION)
|
||||
public static BeatmapComponents DecodeFromLegacy(Stream stream, IResourceStore<byte[]> beatmapsResourceStore, string name, int version = LegacyDecoder<Beatmap>.LATEST_VERSION)
|
||||
{
|
||||
using (var reader = new LineBufferedReader(stream))
|
||||
{
|
||||
@@ -297,7 +301,9 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
var beatmapSkin = new TestLegacySkin(beatmapsResourceStore, name);
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
beatmapSkin.Configuration = new LegacySkinDecoder().Decode(reader);
|
||||
return (convert(beatmap), beatmapSkin);
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
var storyboard = new LegacyStoryboardDecoder().Decode(reader);
|
||||
return new BeatmapComponents(convert(beatmap), beatmapSkin, storyboard);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,13 +315,13 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
}
|
||||
}
|
||||
|
||||
public static MemoryStream EncodeToLegacy((IBeatmap beatmap, ISkin skin) fullBeatmap)
|
||||
public static MemoryStream EncodeToLegacy(BeatmapComponents fullBeatmap)
|
||||
{
|
||||
var (beatmap, beatmapSkin) = fullBeatmap;
|
||||
var (beatmap, beatmapSkin, storyboard) = fullBeatmap;
|
||||
var stream = new MemoryStream();
|
||||
|
||||
using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true))
|
||||
new LegacyBeatmapEncoder(beatmap, beatmapSkin).Encode(writer);
|
||||
new LegacyBeatmapEncoder(beatmap, beatmapSkin, storyboard).Encode(writer);
|
||||
|
||||
stream.Position = 0;
|
||||
|
||||
|
||||
@@ -0,0 +1,400 @@
|
||||
// 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.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Formats;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.Storyboards;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Tests.Beatmaps.Formats
|
||||
{
|
||||
[TestFixture]
|
||||
public class LegacyStoryboardEncoderTest
|
||||
{
|
||||
[Test]
|
||||
public void TestBackground()
|
||||
{
|
||||
var initial = createComponents();
|
||||
initial.Beatmap.BeatmapInfo.Metadata.BackgroundFile = "bg.jpg";
|
||||
|
||||
var encoded = encode(initial);
|
||||
var decodedAfterEncode = decode(encoded);
|
||||
|
||||
Assert.That(decodedAfterEncode.Beatmap.BeatmapInfo.Metadata.BackgroundFile, Is.EqualTo("bg.jpg"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBackgroundOffset()
|
||||
{
|
||||
var initial = createComponents();
|
||||
initial.Beatmap.BeatmapInfo.Metadata.BackgroundFile = "bg_offset.jpg";
|
||||
initial.Storyboard.BackgroundOffset = new Vector2(0, 45);
|
||||
|
||||
var encoded = encode(initial);
|
||||
var decodedAfterEncode = decode(encoded);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(decodedAfterEncode.Beatmap.BeatmapInfo.Metadata.BackgroundFile, Is.EqualTo("bg_offset.jpg"));
|
||||
Assert.That(decodedAfterEncode.Storyboard.BackgroundOffset, Is.EqualTo(new Vector2(0, 45)));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestVideos()
|
||||
{
|
||||
var initial = createComponents();
|
||||
|
||||
initial.Storyboard.GetLayer("Video").Add(new StoryboardVideo(StoryboardElementSource.Beatmap, "video1.avi", 0));
|
||||
initial.Storyboard.GetLayer("Video").Add(new StoryboardVideo(StoryboardElementSource.Shared, "video2.mp4", 1234));
|
||||
|
||||
var encoded = encode(initial);
|
||||
var decodedAfterEncode = decode(encoded);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
var videoLayer = decodedAfterEncode.Storyboard.GetLayer("Video");
|
||||
Assert.That(videoLayer.Elements, Has.Count.EqualTo(2));
|
||||
|
||||
Assert.That(videoLayer.Elements[0].Source, Is.EqualTo(StoryboardElementSource.Beatmap));
|
||||
Assert.That(videoLayer.Elements[0].Path, Is.EqualTo("video1.avi"));
|
||||
Assert.That(videoLayer.Elements[0].StartTime, Is.EqualTo(0));
|
||||
|
||||
Assert.That(videoLayer.Elements[1].Source, Is.EqualTo(StoryboardElementSource.Shared));
|
||||
Assert.That(videoLayer.Elements[1].Path, Is.EqualTo("video2.mp4"));
|
||||
Assert.That(videoLayer.Elements[1].StartTime, Is.EqualTo(1234));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestVideoWithCommands()
|
||||
{
|
||||
var initial = createComponents();
|
||||
|
||||
var video = new StoryboardVideo(StoryboardElementSource.Beatmap, "video1.avi", 0);
|
||||
video.Commands.AddScale(Easing.None, 0, 0, 0.7f, 0.7f);
|
||||
initial.Storyboard.GetLayer("Video").Add(video);
|
||||
|
||||
var encoded = encode(initial);
|
||||
var decodedAfterEncode = decode(encoded);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
var decodedVideo = (StoryboardVideo)decodedAfterEncode.Storyboard.GetLayer("Video").Elements.Single();
|
||||
|
||||
Assert.That(decodedVideo.Source, Is.EqualTo(StoryboardElementSource.Beatmap));
|
||||
Assert.That(decodedVideo.Path, Is.EqualTo("video1.avi"));
|
||||
Assert.That(decodedVideo.StartTime, Is.EqualTo(0));
|
||||
|
||||
Assert.That(decodedVideo.Commands.Scale, Has.Count.EqualTo(1));
|
||||
var scaleCommand = (decodedVideo.Commands.Scale.Single());
|
||||
Assert.That(scaleCommand.Easing, Is.EqualTo(Easing.None));
|
||||
Assert.That(scaleCommand.StartTime, Is.EqualTo(0));
|
||||
Assert.That(scaleCommand.EndTime, Is.EqualTo(0));
|
||||
Assert.That(scaleCommand.StartValue, Is.EqualTo(0.7f));
|
||||
Assert.That(scaleCommand.EndValue, Is.EqualTo(0.7f));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSpritesAndAnimations()
|
||||
{
|
||||
var initial = createComponents();
|
||||
|
||||
initial.Storyboard.GetLayer("Background").Add(new StoryboardSprite(StoryboardElementSource.Beatmap, "1.png", Anchor.TopLeft, new Vector2()));
|
||||
initial.Storyboard.GetLayer("Fail").Add(new StoryboardSprite(StoryboardElementSource.Shared, "2.png", Anchor.Centre, new Vector2(-3)));
|
||||
initial.Storyboard.GetLayer("Pass").Add(new StoryboardSprite(StoryboardElementSource.Shared, "3.png", Anchor.BottomRight, new Vector2(30, -30)));
|
||||
initial.Storyboard.GetLayer("Foreground").Add(new StoryboardAnimation(StoryboardElementSource.Beatmap, "anim1", Anchor.CentreLeft, new Vector2(30), frameCount: 10, frameDelay: 30, AnimationLoopType.LoopForever));
|
||||
initial.Storyboard.GetLayer("Overlay").Add(new StoryboardAnimation(StoryboardElementSource.Shared, "anim2", Anchor.CentreRight, new Vector2(30), frameCount: 4, frameDelay: 100, AnimationLoopType.LoopOnce));
|
||||
|
||||
var encoded = encode(initial);
|
||||
var decodedAfterEncode = decode(encoded);
|
||||
|
||||
var sb = decodedAfterEncode.Storyboard;
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
var backgroundSprite = (StoryboardSprite)sb.GetLayer("Background").Elements.Single();
|
||||
Assert.That(backgroundSprite.Source, Is.EqualTo(StoryboardElementSource.Beatmap));
|
||||
Assert.That(backgroundSprite.Path, Is.EqualTo("1.png"));
|
||||
Assert.That(backgroundSprite.Origin, Is.EqualTo(Anchor.TopLeft));
|
||||
Assert.That(backgroundSprite.InitialPosition, Is.EqualTo(new Vector2()));
|
||||
|
||||
var failSprite = (StoryboardSprite)sb.GetLayer("Fail").Elements.Single();
|
||||
Assert.That(failSprite.Source, Is.EqualTo(StoryboardElementSource.Shared));
|
||||
Assert.That(failSprite.Path, Is.EqualTo("2.png"));
|
||||
Assert.That(failSprite.Origin, Is.EqualTo(Anchor.Centre));
|
||||
Assert.That(failSprite.InitialPosition, Is.EqualTo(new Vector2(-3)));
|
||||
|
||||
var passSprite = (StoryboardSprite)sb.GetLayer("Pass").Elements.Single();
|
||||
Assert.That(passSprite.Source, Is.EqualTo(StoryboardElementSource.Shared));
|
||||
Assert.That(passSprite.Path, Is.EqualTo("3.png"));
|
||||
Assert.That(passSprite.Origin, Is.EqualTo(Anchor.BottomRight));
|
||||
Assert.That(passSprite.InitialPosition, Is.EqualTo(new Vector2(30, -30)));
|
||||
|
||||
var foregroundAnimation = (StoryboardAnimation)sb.GetLayer("Foreground").Elements.Single();
|
||||
Assert.That(foregroundAnimation.Source, Is.EqualTo(StoryboardElementSource.Beatmap));
|
||||
Assert.That(foregroundAnimation.Path, Is.EqualTo("anim1"));
|
||||
Assert.That(foregroundAnimation.Origin, Is.EqualTo(Anchor.CentreLeft));
|
||||
Assert.That(foregroundAnimation.InitialPosition, Is.EqualTo(new Vector2(30)));
|
||||
Assert.That(foregroundAnimation.FrameCount, Is.EqualTo(10));
|
||||
Assert.That(foregroundAnimation.FrameDelay, Is.EqualTo(30));
|
||||
Assert.That(foregroundAnimation.LoopType, Is.EqualTo(AnimationLoopType.LoopForever));
|
||||
|
||||
var overlayAnimation = (StoryboardAnimation)sb.GetLayer("Overlay").Elements.Single();
|
||||
Assert.That(overlayAnimation.Source, Is.EqualTo(StoryboardElementSource.Shared));
|
||||
Assert.That(overlayAnimation.Path, Is.EqualTo("anim2"));
|
||||
Assert.That(overlayAnimation.Origin, Is.EqualTo(Anchor.CentreRight));
|
||||
Assert.That(overlayAnimation.InitialPosition, Is.EqualTo(new Vector2(30)));
|
||||
Assert.That(overlayAnimation.FrameCount, Is.EqualTo(4));
|
||||
Assert.That(overlayAnimation.FrameDelay, Is.EqualTo(100));
|
||||
Assert.That(overlayAnimation.LoopType, Is.EqualTo(AnimationLoopType.LoopOnce));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCommands()
|
||||
{
|
||||
var initial = createComponents();
|
||||
|
||||
var sprite = new StoryboardSprite(StoryboardElementSource.Beatmap, "test.jpg", Anchor.Centre, new Vector2(300));
|
||||
sprite.Commands.AddAlpha(Easing.InBack, 100, 200, 0, 1);
|
||||
sprite.Commands.AddBlendingParameters(Easing.None, 300, 300, BlendingParameters.Additive, BlendingParameters.Additive);
|
||||
sprite.Commands.AddColour(Easing.InCubic, 400, 500, Color4.White, Color4.Aquamarine);
|
||||
sprite.Commands.AddFlipH(Easing.InOutQuad, 600, 600, true, true);
|
||||
sprite.Commands.AddFlipV(Easing.InOutQuad, 800, 900, true, false);
|
||||
sprite.Commands.AddRotation(Easing.OutSine, 1000, 1100, 0, 720);
|
||||
sprite.Commands.AddScale(Easing.OutQuint, 1200, 1300, 1, 4);
|
||||
sprite.Commands.AddVectorScale(Easing.InCirc, 1400, 1500, new Vector2(4), new Vector2(3, 1));
|
||||
sprite.Commands.AddX(Easing.InOutQuad, 1600, 1700, 300, 500);
|
||||
sprite.Commands.AddY(Easing.OutBounce, 1800, 1800, 300, 100);
|
||||
initial.Storyboard.GetLayer("Background").Add(sprite);
|
||||
|
||||
var encoded = encode(initial);
|
||||
var decodedAfterEncode = decode(encoded);
|
||||
|
||||
var decodedSprite = (StoryboardSprite)decodedAfterEncode.Storyboard.GetLayer("Background").Elements.Single();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
var alphaCommand = decodedSprite.Commands.Alpha.Single();
|
||||
Assert.That(alphaCommand.Easing, Is.EqualTo(Easing.InBack));
|
||||
Assert.That(alphaCommand.StartTime, Is.EqualTo(100));
|
||||
Assert.That(alphaCommand.EndTime, Is.EqualTo(200));
|
||||
Assert.That(alphaCommand.StartValue, Is.EqualTo(0));
|
||||
Assert.That(alphaCommand.EndValue, Is.EqualTo(1));
|
||||
|
||||
var blendingCommand = decodedSprite.Commands.BlendingParameters.Single();
|
||||
Assert.That(blendingCommand.Easing, Is.EqualTo(Easing.None));
|
||||
Assert.That(blendingCommand.StartTime, Is.EqualTo(300));
|
||||
Assert.That(blendingCommand.EndTime, Is.EqualTo(300));
|
||||
Assert.That(blendingCommand.StartValue, Is.EqualTo(BlendingParameters.Additive));
|
||||
Assert.That(blendingCommand.EndValue, Is.EqualTo(BlendingParameters.Additive));
|
||||
|
||||
var colourCommand = decodedSprite.Commands.Colour.Single();
|
||||
Assert.That(colourCommand.Easing, Is.EqualTo(Easing.InCubic));
|
||||
Assert.That(colourCommand.StartTime, Is.EqualTo(400));
|
||||
Assert.That(colourCommand.EndTime, Is.EqualTo(500));
|
||||
Assert.That(colourCommand.StartValue, Is.EqualTo(Color4.White));
|
||||
Assert.That(colourCommand.EndValue, Is.EqualTo(Color4.Aquamarine));
|
||||
|
||||
var flipHCommand = decodedSprite.Commands.FlipH.Single();
|
||||
Assert.That(flipHCommand.Easing, Is.EqualTo(Easing.InOutQuad));
|
||||
Assert.That(flipHCommand.StartTime, Is.EqualTo(600));
|
||||
Assert.That(flipHCommand.EndTime, Is.EqualTo(600));
|
||||
Assert.That(flipHCommand.StartValue, Is.EqualTo(true));
|
||||
Assert.That(flipHCommand.EndValue, Is.EqualTo(true));
|
||||
|
||||
var flipVCommand = decodedSprite.Commands.FlipV.Single();
|
||||
Assert.That(flipVCommand.Easing, Is.EqualTo(Easing.InOutQuad));
|
||||
Assert.That(flipVCommand.StartTime, Is.EqualTo(800));
|
||||
Assert.That(flipVCommand.EndTime, Is.EqualTo(900));
|
||||
Assert.That(flipVCommand.StartValue, Is.EqualTo(true));
|
||||
Assert.That(flipVCommand.EndValue, Is.EqualTo(false));
|
||||
|
||||
var rotationCommand = decodedSprite.Commands.Rotation.Single();
|
||||
Assert.That(rotationCommand.Easing, Is.EqualTo(Easing.OutSine));
|
||||
Assert.That(rotationCommand.StartTime, Is.EqualTo(1000));
|
||||
Assert.That(rotationCommand.EndTime, Is.EqualTo(1100));
|
||||
Assert.That(rotationCommand.StartValue, Is.EqualTo(0));
|
||||
Assert.That(rotationCommand.EndValue, Is.EqualTo(720));
|
||||
|
||||
var scaleCommand = decodedSprite.Commands.Scale.Single();
|
||||
Assert.That(scaleCommand.Easing, Is.EqualTo(Easing.OutQuint));
|
||||
Assert.That(scaleCommand.StartTime, Is.EqualTo(1200));
|
||||
Assert.That(scaleCommand.EndTime, Is.EqualTo(1300));
|
||||
Assert.That(scaleCommand.StartValue, Is.EqualTo(1));
|
||||
Assert.That(scaleCommand.EndValue, Is.EqualTo(4));
|
||||
|
||||
var vectorScaleCommand = decodedSprite.Commands.VectorScale.Single();
|
||||
Assert.That(vectorScaleCommand.Easing, Is.EqualTo(Easing.InCirc));
|
||||
Assert.That(vectorScaleCommand.StartTime, Is.EqualTo(1400));
|
||||
Assert.That(vectorScaleCommand.EndTime, Is.EqualTo(1500));
|
||||
Assert.That(vectorScaleCommand.StartValue, Is.EqualTo(new Vector2(4)));
|
||||
Assert.That(vectorScaleCommand.EndValue, Is.EqualTo(new Vector2(3, 1)));
|
||||
|
||||
var xCommand = decodedSprite.Commands.X.Single();
|
||||
Assert.That(xCommand.Easing, Is.EqualTo(Easing.InOutQuad));
|
||||
Assert.That(xCommand.StartTime, Is.EqualTo(1600));
|
||||
Assert.That(xCommand.EndTime, Is.EqualTo(1700));
|
||||
Assert.That(xCommand.StartValue, Is.EqualTo(300));
|
||||
Assert.That(xCommand.EndValue, Is.EqualTo(500));
|
||||
|
||||
var yCommand = decodedSprite.Commands.Y.Single();
|
||||
Assert.That(yCommand.Easing, Is.EqualTo(Easing.OutBounce));
|
||||
Assert.That(yCommand.StartTime, Is.EqualTo(1800));
|
||||
Assert.That(yCommand.EndTime, Is.EqualTo(1800));
|
||||
Assert.That(yCommand.StartValue, Is.EqualTo(300));
|
||||
Assert.That(yCommand.EndValue, Is.EqualTo(100));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLoopingGroup()
|
||||
{
|
||||
var initial = createComponents();
|
||||
|
||||
var sprite = new StoryboardSprite(StoryboardElementSource.Beatmap, "test.jpg", Anchor.Centre, new Vector2(300));
|
||||
var loopingGroup = sprite.AddLoopingGroup(1000, 44);
|
||||
loopingGroup.AddAlpha(Easing.OutQuint, 1000, 1500, 0, 1);
|
||||
initial.Storyboard.GetLayer("Background").Add(sprite);
|
||||
|
||||
var encoded = encode(initial);
|
||||
var decodedAfterEncode = decode(encoded);
|
||||
|
||||
var decodedSprite = (StoryboardSprite)decodedAfterEncode.Storyboard.GetLayer("Background").Elements.Single();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(decodedSprite.LoopingGroups, Has.Count.EqualTo(1));
|
||||
var decodedLoopingGroup = decodedSprite.LoopingGroups.Single();
|
||||
Assert.That(decodedLoopingGroup.StartTime, Is.EqualTo(1000));
|
||||
Assert.That(decodedLoopingGroup.TotalIterations, Is.EqualTo(45));
|
||||
|
||||
var alphaCommand = decodedLoopingGroup.Alpha.Single();
|
||||
Assert.That(alphaCommand.Easing, Is.EqualTo(Easing.OutQuint));
|
||||
Assert.That(alphaCommand.StartTime, Is.EqualTo(1000));
|
||||
Assert.That(alphaCommand.EndTime, Is.EqualTo(1500));
|
||||
Assert.That(alphaCommand.StartValue, Is.EqualTo(0));
|
||||
Assert.That(alphaCommand.EndValue, Is.EqualTo(1));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTriggerGroup()
|
||||
{
|
||||
var initial = createComponents();
|
||||
|
||||
var sprite = new StoryboardSprite(StoryboardElementSource.Beatmap, "test.jpg", Anchor.Centre, new Vector2(300));
|
||||
var triggerGroup = sprite.AddTriggerGroup("Passing", 0, 100000, 33);
|
||||
triggerGroup.AddAlpha(Easing.OutQuint, 0, 500, 0, 1);
|
||||
initial.Storyboard.GetLayer("Background").Add(sprite);
|
||||
|
||||
var encoded = encode(initial);
|
||||
var decodedAfterEncode = decode(encoded);
|
||||
|
||||
var decodedSprite = (StoryboardSprite)decodedAfterEncode.Storyboard.GetLayer("Background").Elements.Single();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(decodedSprite.TriggerGroups, Has.Count.EqualTo(1));
|
||||
var decodedTriggerGroup = decodedSprite.TriggerGroups.Single();
|
||||
Assert.That(decodedTriggerGroup.TriggerName, Is.EqualTo("Passing"));
|
||||
Assert.That(decodedTriggerGroup.TriggerStartTime, Is.EqualTo(0));
|
||||
Assert.That(decodedTriggerGroup.TriggerEndTime, Is.EqualTo(100000));
|
||||
Assert.That(decodedTriggerGroup.GroupNumber, Is.EqualTo(33));
|
||||
|
||||
var alphaCommand = decodedTriggerGroup.Alpha.Single();
|
||||
Assert.That(alphaCommand.Easing, Is.EqualTo(Easing.OutQuint));
|
||||
Assert.That(alphaCommand.StartTime, Is.EqualTo(0));
|
||||
Assert.That(alphaCommand.EndTime, Is.EqualTo(500));
|
||||
Assert.That(alphaCommand.StartValue, Is.EqualTo(0));
|
||||
Assert.That(alphaCommand.EndValue, Is.EqualTo(1));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestStoryboardSamples()
|
||||
{
|
||||
var initial = createComponents();
|
||||
|
||||
initial.Storyboard.GetLayer("Pass").Add(new StoryboardSampleInfo(StoryboardElementSource.Beatmap, "pass.wav", 4000, 85));
|
||||
initial.Storyboard.GetLayer("Fail").Add(new StoryboardSampleInfo(StoryboardElementSource.Shared, "fail.wav", 4000, 100));
|
||||
|
||||
var encoded = encode(initial);
|
||||
var decodedAfterEncode = decode(encoded);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
var passingSample = (StoryboardSampleInfo)decodedAfterEncode.Storyboard.GetLayer("Pass").Elements.Single();
|
||||
Assert.That(passingSample.Source, Is.EqualTo(StoryboardElementSource.Beatmap));
|
||||
Assert.That(passingSample.Path, Is.EqualTo("pass.wav"));
|
||||
Assert.That(passingSample.StartTime, Is.EqualTo(4000));
|
||||
Assert.That(passingSample.Volume, Is.EqualTo(85));
|
||||
|
||||
var failingSample = (StoryboardSampleInfo)decodedAfterEncode.Storyboard.GetLayer("Fail").Elements.Single();
|
||||
Assert.That(failingSample.Source, Is.EqualTo(StoryboardElementSource.Shared));
|
||||
Assert.That(failingSample.Path, Is.EqualTo("fail.wav"));
|
||||
Assert.That(failingSample.StartTime, Is.EqualTo(4000));
|
||||
Assert.That(failingSample.Volume, Is.EqualTo(100));
|
||||
});
|
||||
}
|
||||
|
||||
private record DecodedBeatmapComponents(IBeatmap Beatmap, Storyboard Storyboard);
|
||||
|
||||
private record EncodedBeatmapComponents(MemoryStream Beatmap, MemoryStream Storyboard);
|
||||
|
||||
private DecodedBeatmapComponents createComponents()
|
||||
{
|
||||
var beatmapInfo = new BeatmapInfo();
|
||||
var beatmap = new Beatmap
|
||||
{
|
||||
BeatmapInfo = beatmapInfo
|
||||
};
|
||||
var storyboard = new Storyboard
|
||||
{
|
||||
Beatmap = beatmap,
|
||||
BeatmapInfo = beatmapInfo
|
||||
};
|
||||
|
||||
return new DecodedBeatmapComponents(beatmap, storyboard);
|
||||
}
|
||||
|
||||
private EncodedBeatmapComponents encode(DecodedBeatmapComponents decoded)
|
||||
{
|
||||
var beatmapStream = new MemoryStream();
|
||||
using (var beatmapWriter = new StreamWriter(beatmapStream, Encoding.UTF8, 1024, leaveOpen: true))
|
||||
new LegacyBeatmapEncoder(decoded.Beatmap, null, decoded.Storyboard).Encode(beatmapWriter);
|
||||
beatmapStream.Position = 0;
|
||||
|
||||
var storyboardStream = new MemoryStream();
|
||||
using (var storyboardWriter = new StreamWriter(storyboardStream, Encoding.UTF8, 1024, leaveOpen: true))
|
||||
new LegacyStoryboardEncoder(decoded.Storyboard).EncodeStandaloneStoryboard(storyboardWriter);
|
||||
storyboardStream.Position = 0;
|
||||
|
||||
return new EncodedBeatmapComponents(beatmapStream, storyboardStream);
|
||||
}
|
||||
|
||||
private DecodedBeatmapComponents decode(EncodedBeatmapComponents encoded)
|
||||
{
|
||||
using var beatmapReader = new LineBufferedReader(encoded.Beatmap, leaveOpen: true);
|
||||
var beatmap = new LegacyBeatmapDecoder().Decode(beatmapReader);
|
||||
|
||||
encoded.Beatmap.Position = 0;
|
||||
using var storyboardReader = new LineBufferedReader(encoded.Storyboard, leaveOpen: true);
|
||||
var storyboard = new LegacyStoryboardDecoder().Decode(beatmapReader, storyboardReader);
|
||||
|
||||
encoded.Beatmap.Position = 0;
|
||||
encoded.Storyboard.Position = 0;
|
||||
|
||||
return new DecodedBeatmapComponents(beatmap, storyboard);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,7 @@ namespace osu.Game.Tests.Editing.Checks
|
||||
{
|
||||
var storyboard = new Storyboard();
|
||||
var layer = storyboard.GetLayer("Video");
|
||||
layer.Add(new StoryboardVideo("abc123.mp4", 0));
|
||||
layer.Add(new StoryboardVideo(StoryboardElementSource.Beatmap, "abc123.mp4", 0));
|
||||
|
||||
var mockWorkingBeatmap = new Mock<TestWorkingBeatmap>(beatmap, null!, null!);
|
||||
mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny<string>())).Returns(resourceStream);
|
||||
|
||||
@@ -7,9 +7,11 @@ using NUnit.Framework;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
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.Edit.Checks;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
|
||||
@@ -18,7 +20,7 @@ namespace osu.Game.Tests.Editing.Checks
|
||||
[TestFixture]
|
||||
public class CheckFewHitsoundsTest
|
||||
{
|
||||
private CheckFewHitsounds check = null!;
|
||||
private CheckOsuFewHitsounds check = null!;
|
||||
|
||||
private List<HitSampleInfo> notHitsounded = null!;
|
||||
private List<HitSampleInfo> hitsounded = null!;
|
||||
@@ -26,7 +28,7 @@ namespace osu.Game.Tests.Editing.Checks
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
check = new CheckFewHitsounds();
|
||||
check = new CheckOsuFewHitsounds();
|
||||
notHitsounded = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) };
|
||||
hitsounded = new List<HitSampleInfo>
|
||||
{
|
||||
@@ -82,6 +84,43 @@ namespace osu.Game.Tests.Editing.Checks
|
||||
assertOk(hitObjects);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRarelyHitsoundedLongWallTimeMostlyBreak()
|
||||
{
|
||||
var hitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Samples = hitsounded },
|
||||
new HitCircle { StartTime = 1000, Samples = notHitsounded },
|
||||
new HitCircle { StartTime = 2000, Samples = notHitsounded },
|
||||
new HitCircle { StartTime = 3000, Samples = notHitsounded },
|
||||
new HitCircle { StartTime = 4000, Samples = notHitsounded },
|
||||
new HitCircle { StartTime = 5000, Samples = notHitsounded },
|
||||
new HitCircle { StartTime = 10000, Samples = hitsounded },
|
||||
};
|
||||
|
||||
// 10s since last hitsound, but 6s overlap a break → 4s without hitsounds (below warning threshold).
|
||||
assertOk(hitObjects, new BreakPeriod(4000, 10000));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRarelyHitsoundedLongWallTimeMostlySpinner()
|
||||
{
|
||||
var hitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Samples = hitsounded },
|
||||
new HitCircle { StartTime = 200, Samples = notHitsounded },
|
||||
new HitCircle { StartTime = 400, Samples = notHitsounded },
|
||||
new HitCircle { StartTime = 600, Samples = notHitsounded },
|
||||
new HitCircle { StartTime = 800, Samples = notHitsounded },
|
||||
new HitCircle { StartTime = 1000, Samples = notHitsounded },
|
||||
new Spinner { StartTime = 1200, EndTime = 21200, Samples = notHitsounded },
|
||||
new HitCircle { StartTime = 21400, Samples = hitsounded },
|
||||
};
|
||||
|
||||
// 21.4s since last hitsound, but 20s overlap a spinner → 1.4s without hitsounds.
|
||||
assertOk(hitObjects);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLightlyHitsounded()
|
||||
{
|
||||
@@ -194,9 +233,9 @@ namespace osu.Game.Tests.Editing.Checks
|
||||
assertOk(hitObjects);
|
||||
}
|
||||
|
||||
private void assertOk(List<HitObject> hitObjects)
|
||||
private void assertOk(List<HitObject> hitObjects, params BreakPeriod[] breaks)
|
||||
{
|
||||
Assert.That(check.Run(getContext(hitObjects)), Is.Empty);
|
||||
Assert.That(check.Run(getContext(hitObjects, breaks)), Is.Empty);
|
||||
}
|
||||
|
||||
private void assertLongPeriodProblem(List<HitObject> hitObjects, int count = 1)
|
||||
@@ -231,10 +270,13 @@ namespace osu.Game.Tests.Editing.Checks
|
||||
Assert.That(issues.Any(issue => issue.Template is CheckFewHitsounds.IssueTemplateNoHitsounds));
|
||||
}
|
||||
|
||||
private BeatmapVerifierContext getContext(List<HitObject> hitObjects)
|
||||
private BeatmapVerifierContext getContext(List<HitObject> hitObjects, params BreakPeriod[] breaks)
|
||||
{
|
||||
var beatmap = new Beatmap<HitObject> { HitObjects = hitObjects };
|
||||
|
||||
foreach (var b in breaks)
|
||||
beatmap.Breaks.Add(b);
|
||||
|
||||
return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,7 +260,7 @@ namespace osu.Game.Tests.Editing.Checks
|
||||
if (hasStoryboard)
|
||||
{
|
||||
storyboard = new Storyboard();
|
||||
storyboard.GetLayer("Background").Add(new StoryboardSprite("test.png", Anchor.Centre, Vector2.Zero));
|
||||
storyboard.GetLayer("Background").Add(new StoryboardSprite(StoryboardElementSource.Beatmap, "test.png", Anchor.Centre, Vector2.Zero));
|
||||
}
|
||||
|
||||
var verifiedCurrentBeatmap = new BeatmapVerifierContext.VerifiedBeatmap(new TestWorkingBeatmap(currentBeatmap, storyboard), currentBeatmap);
|
||||
|
||||
@@ -80,7 +80,7 @@ namespace osu.Game.Tests.Editing.Checks
|
||||
{
|
||||
var storyboard = new Storyboard();
|
||||
|
||||
var video = new StoryboardVideo("abc123.mp4", 0);
|
||||
var video = new StoryboardVideo(StoryboardElementSource.Beatmap, "abc123.mp4", 0);
|
||||
|
||||
storyboard.GetLayer("Video").Add(video);
|
||||
|
||||
@@ -98,7 +98,7 @@ namespace osu.Game.Tests.Editing.Checks
|
||||
{
|
||||
var storyboard = new Storyboard();
|
||||
|
||||
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
|
||||
var sprite = new StoryboardSprite(StoryboardElementSource.Beatmap, "unknown", Anchor.TopLeft, Vector2.Zero);
|
||||
|
||||
storyboard.GetLayer("Background").Add(sprite);
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ namespace osu.Game.Tests.Editing.Checks
|
||||
{
|
||||
var storyboard = new Storyboard();
|
||||
var layer = storyboard.GetLayer("Video");
|
||||
layer.Add(new StoryboardVideo("abc123.mp4", 0));
|
||||
layer.Add(new StoryboardVideo(StoryboardElementSource.Beatmap, "abc123.mp4", 0));
|
||||
|
||||
var mockWorkingBeatmap = new Mock<TestWorkingBeatmap>(beatmap, null!, null!);
|
||||
mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny<string>())).Returns(resourceStream);
|
||||
|
||||
@@ -143,7 +143,7 @@ namespace osu.Game.Tests.Editing.Checks
|
||||
};
|
||||
|
||||
var storyboard = new Storyboard();
|
||||
storyboard.GetLayer("Video").Add(new StoryboardVideo(path, startTime));
|
||||
storyboard.GetLayer("Video").Add(new StoryboardVideo(StoryboardElementSource.Beatmap, path, startTime));
|
||||
|
||||
var working = new TestWorkingBeatmap(beatmap, storyboard);
|
||||
return new BeatmapVerifierContext.VerifiedBeatmap(working, beatmap);
|
||||
|
||||
@@ -362,7 +362,7 @@ namespace osu.Game.Tests.Editing
|
||||
using (var encoded = new MemoryStream())
|
||||
{
|
||||
using (var sw = new StreamWriter(encoded))
|
||||
new LegacyBeatmapEncoder(beatmap, null).Encode(sw);
|
||||
new LegacyBeatmapEncoder(beatmap, null, null).Encode(sw);
|
||||
|
||||
return encoded.ToArray();
|
||||
}
|
||||
|
||||
@@ -51,7 +51,15 @@ namespace osu.Game.Tests.Extensions
|
||||
[TestCase(0.4, true, 2, ExpectedResult = "40%")]
|
||||
[TestCase(1e-6, false, 6, ExpectedResult = "0,000001")]
|
||||
[TestCase(0.48333, true, 4, ExpectedResult = "48,33%")]
|
||||
public string TestCultureSensitivity(double input, bool percent, int decimalDigits)
|
||||
public string TestCultureSensitivityDecimalPoint(double input, bool percent, int decimalDigits)
|
||||
{
|
||||
return input.ToStandardFormattedString(decimalDigits, percent);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[SetCulture("sv-SE")]
|
||||
[TestCase(-1e-6, false, 6, ExpectedResult = "−0,000001")]
|
||||
public string TestCultureSensitivityNegativeSign(double input, bool percent, int decimalDigits)
|
||||
{
|
||||
return input.ToStandardFormattedString(decimalDigits, percent);
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ namespace osu.Game.Tests.Gameplay
|
||||
{
|
||||
Child = new FrameStabilityContainer
|
||||
{
|
||||
Child = sample = new DrawableStoryboardSample(new StoryboardSampleInfo(string.Empty, 0, 1))
|
||||
Child = sample = new DrawableStoryboardSample(new StoryboardSampleInfo(StoryboardElementSource.Beatmap, string.Empty, 0, 1))
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -109,7 +109,7 @@ namespace osu.Game.Tests.Gameplay
|
||||
{
|
||||
Child = new FrameStabilityContainer
|
||||
{
|
||||
Child = sample = new DrawableStoryboardSample(new StoryboardSampleInfo(string.Empty, 0, 1))
|
||||
Child = sample = new DrawableStoryboardSample(new StoryboardSampleInfo(StoryboardElementSource.Beatmap, string.Empty, 0, 1))
|
||||
}
|
||||
});
|
||||
|
||||
@@ -141,7 +141,7 @@ namespace osu.Game.Tests.Gameplay
|
||||
Child = beatmapSkinSourceContainer
|
||||
});
|
||||
|
||||
beatmapSkinSourceContainer.Add(sample = new DrawableStoryboardSample(new StoryboardSampleInfo("test-sample", 1, 1))
|
||||
beatmapSkinSourceContainer.Add(sample = new DrawableStoryboardSample(new StoryboardSampleInfo(StoryboardElementSource.Beatmap, "test-sample", 1, 1))
|
||||
{
|
||||
Clock = gameplayContainer
|
||||
});
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
// 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.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Game.IPC;
|
||||
|
||||
namespace osu.Game.Tests.IPC
|
||||
{
|
||||
public sealed class WebSocketClient : IDisposable
|
||||
{
|
||||
public event Action<string>? MessageReceived;
|
||||
public event Action? Closed;
|
||||
|
||||
private readonly int port;
|
||||
private WebSocketChannel? channel;
|
||||
|
||||
public WebSocketClient(int port)
|
||||
{
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public async Task Start(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var webSocket = new ClientWebSocket();
|
||||
await webSocket.ConnectAsync(new Uri($@"ws://localhost:{port}/"), cancellationToken);
|
||||
channel = new WebSocketChannel(webSocket);
|
||||
channel.MessageReceived += msg => MessageReceived?.Invoke(msg);
|
||||
channel.ClosedPrematurely += () => Closed?.Invoke();
|
||||
channel.Start(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task SendAsync(string message)
|
||||
{
|
||||
if (channel == null)
|
||||
throw new InvalidOperationException($@"Must {nameof(Start)} first.");
|
||||
|
||||
await channel.SendAsync(message);
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken stoppingToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (channel != null)
|
||||
await channel.StopAsync(stoppingToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// has to be caught manually because outer task isn't accepting `stoppingToken`.
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
channel?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
// 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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.IPC;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
|
||||
namespace osu.Game.Tests.IPC
|
||||
{
|
||||
[TestFixture]
|
||||
public class WebSocketTest
|
||||
{
|
||||
[Test]
|
||||
public async Task TestClientInitiatedDuplexCommunication()
|
||||
{
|
||||
const int port = 54321;
|
||||
|
||||
var server = new WebSocketServer(port);
|
||||
var client = new WebSocketClient(port);
|
||||
|
||||
var duplexComplete = new ManualResetEventSlim(false);
|
||||
|
||||
server.MessageReceived += (clientId, msg) =>
|
||||
{
|
||||
if (msg != "PING")
|
||||
return;
|
||||
|
||||
// ReSharper disable once AccessToDisposedClosure
|
||||
server.SendAsync(clientId, "PONG").FireAndForget();
|
||||
};
|
||||
client.MessageReceived += msg =>
|
||||
{
|
||||
if (msg != "PONG")
|
||||
return;
|
||||
|
||||
duplexComplete.Set();
|
||||
};
|
||||
|
||||
await server.StartAsync();
|
||||
await client.Start();
|
||||
|
||||
await client.SendAsync("PING");
|
||||
Assert.That(duplexComplete.Wait(10_000));
|
||||
|
||||
await client.StopAsync();
|
||||
await server.StopAsync();
|
||||
|
||||
client.Dispose();
|
||||
server.Dispose();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestServerInitiatedDuplexCommunication()
|
||||
{
|
||||
const int port = 54321;
|
||||
|
||||
var server = new WebSocketServer(port);
|
||||
var client = new WebSocketClient(port);
|
||||
|
||||
var clientConnected = new ManualResetEventSlim();
|
||||
var duplexComplete = new ManualResetEventSlim();
|
||||
|
||||
client.MessageReceived += msg =>
|
||||
{
|
||||
if (msg != "PING")
|
||||
return;
|
||||
|
||||
// ReSharper disable once AccessToDisposedClosure
|
||||
client.SendAsync("PONG").FireAndForget();
|
||||
};
|
||||
server.ClientConnected += _ => clientConnected.Set();
|
||||
server.MessageReceived += (_, msg) =>
|
||||
{
|
||||
if (msg != "PONG")
|
||||
return;
|
||||
|
||||
duplexComplete.Set();
|
||||
};
|
||||
|
||||
await server.StartAsync();
|
||||
await client.Start();
|
||||
Assert.That(clientConnected.Wait(10_000));
|
||||
|
||||
await server.SendAsync(1, "PING");
|
||||
Assert.That(duplexComplete.Wait(10_000));
|
||||
|
||||
await client.StopAsync();
|
||||
await server.StopAsync();
|
||||
|
||||
client.Dispose();
|
||||
server.Dispose();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestServerBroadcast()
|
||||
{
|
||||
const int port = 54321;
|
||||
const int client_count = 5;
|
||||
|
||||
var server = new WebSocketServer(port);
|
||||
var clients = new List<WebSocketClient>(client_count);
|
||||
var connectionCountdown = new CountdownEvent(client_count);
|
||||
var receiptCountdown = new CountdownEvent(client_count);
|
||||
|
||||
for (int i = 0; i < client_count; ++i)
|
||||
{
|
||||
var client = new WebSocketClient(port);
|
||||
client.MessageReceived += msg =>
|
||||
{
|
||||
if (msg != "HI ALL")
|
||||
return;
|
||||
|
||||
receiptCountdown.Signal();
|
||||
};
|
||||
clients.Add(client);
|
||||
}
|
||||
|
||||
server.ClientConnected += _ => connectionCountdown.Signal();
|
||||
|
||||
await server.StartAsync();
|
||||
|
||||
foreach (var client in clients)
|
||||
await client.Start();
|
||||
Assert.That(connectionCountdown.Wait(10_000));
|
||||
|
||||
await server.BroadcastAsync("HI ALL");
|
||||
Assert.That(receiptCountdown.Wait(10_000));
|
||||
|
||||
foreach (var client in clients)
|
||||
{
|
||||
await client.StopAsync();
|
||||
client.Dispose();
|
||||
}
|
||||
|
||||
await server.StopAsync();
|
||||
server.Dispose();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestClientSoftAborts()
|
||||
{
|
||||
const int port = 54321;
|
||||
|
||||
var server = new WebSocketServer(port);
|
||||
var client = new WebSocketClient(port);
|
||||
|
||||
await server.StartAsync();
|
||||
await client.Start();
|
||||
|
||||
await client.StopAsync();
|
||||
client.Dispose();
|
||||
|
||||
await server.StopAsync();
|
||||
server.Dispose();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestClientHardAborts()
|
||||
{
|
||||
const int port = 54321;
|
||||
|
||||
var server = new WebSocketServer(port);
|
||||
var client = new WebSocketClient(port);
|
||||
|
||||
await server.StartAsync();
|
||||
await client.Start();
|
||||
|
||||
await client.StopAsync(new CancellationToken(true));
|
||||
client.Dispose();
|
||||
|
||||
await server.StopAsync();
|
||||
server.Dispose();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestServerSoftAborts()
|
||||
{
|
||||
const int port = 54321;
|
||||
|
||||
var server = new WebSocketServer(port);
|
||||
var client = new WebSocketClient(port);
|
||||
|
||||
await server.StartAsync();
|
||||
await client.Start();
|
||||
|
||||
await server.StopAsync();
|
||||
server.Dispose();
|
||||
|
||||
await client.StopAsync();
|
||||
client.Dispose();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestServerHardAborts()
|
||||
{
|
||||
const int port = 54321;
|
||||
|
||||
var server = new WebSocketServer(port);
|
||||
var client = new WebSocketClient(port);
|
||||
|
||||
await server.StartAsync();
|
||||
await client.Start();
|
||||
|
||||
await server.StopAsync(new CancellationToken(true));
|
||||
server.Dispose();
|
||||
|
||||
await client.StopAsync();
|
||||
client.Dispose();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestClientMessageTooLong()
|
||||
{
|
||||
const int port = 54321;
|
||||
|
||||
var server = new WebSocketServer(port);
|
||||
var client = new WebSocketClient(port);
|
||||
|
||||
var clientClosed = new ManualResetEventSlim();
|
||||
client.Closed += clientClosed.Set;
|
||||
|
||||
await server.StartAsync();
|
||||
await client.Start();
|
||||
|
||||
await client.SendAsync(new string('0', 9999));
|
||||
Assert.That(clientClosed.Wait(10_000));
|
||||
await client.StopAsync();
|
||||
client.Dispose();
|
||||
|
||||
var client2 = new WebSocketClient(port);
|
||||
|
||||
var duplexComplete = new ManualResetEventSlim();
|
||||
server.MessageReceived += (clientId, msg) =>
|
||||
{
|
||||
if (msg != "PING")
|
||||
return;
|
||||
|
||||
// ReSharper disable once AccessToDisposedClosure
|
||||
server.SendAsync(clientId, "PONG").FireAndForget();
|
||||
};
|
||||
client2.MessageReceived += msg =>
|
||||
{
|
||||
if (msg != "PONG")
|
||||
return;
|
||||
|
||||
duplexComplete.Set();
|
||||
};
|
||||
|
||||
await client2.Start();
|
||||
await client2.SendAsync("PING");
|
||||
Assert.That(duplexComplete.Wait(10000));
|
||||
|
||||
await client2.StopAsync();
|
||||
await server.StopAsync();
|
||||
|
||||
client2.Dispose();
|
||||
server.Dispose();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestStartStopServerWithoutReceivingClients()
|
||||
{
|
||||
const int port = 54321;
|
||||
|
||||
var server = new WebSocketServer(port);
|
||||
await server.StartAsync();
|
||||
await server.StopAsync();
|
||||
server.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,10 @@ using System.Linq;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.TypeExtensions;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Catch;
|
||||
@@ -33,6 +35,17 @@ namespace osu.Game.Tests.Mods
|
||||
Assert.That(invalid, Is.EquivalentTo(new[] { mod.Object }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestModIsNotCompatibleWithItselfEvenIfSettingsDiffer()
|
||||
{
|
||||
var mod1 = new Mock<CustomMod3>();
|
||||
var mod2 = new Mock<CustomMod3>();
|
||||
mod2.Setup(m => m.Setting).Returns(new BindableBool(true));
|
||||
|
||||
Assert.That(ModUtils.CheckCompatibleSet(new[] { mod1.Object, mod2.Object }, out var invalid), Is.False);
|
||||
Assert.That(invalid, Is.EquivalentTo(new[] { mod2.Object }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestModIsCompatibleByItself()
|
||||
{
|
||||
@@ -397,6 +410,12 @@ namespace osu.Game.Tests.Mods
|
||||
{
|
||||
}
|
||||
|
||||
public abstract class CustomMod3 : Mod, IModCompatibilitySpecification
|
||||
{
|
||||
[SettingSource("Setting")]
|
||||
public virtual BindableBool Setting { get; } = new BindableBool();
|
||||
}
|
||||
|
||||
private class InvalidMultiplayerMod : Mod
|
||||
{
|
||||
public override string Name => string.Empty;
|
||||
|
||||
@@ -126,7 +126,7 @@ namespace osu.Game.Tests.NonVisual
|
||||
|
||||
Assert.That(storage.GetFullPath("."), Is.EqualTo(defaultStorageLocation));
|
||||
|
||||
osu.Migrate(customPath);
|
||||
osu.MigrateUserData(customPath);
|
||||
|
||||
Assert.That(storage.GetFullPath("."), Is.EqualTo(customPath));
|
||||
|
||||
@@ -183,16 +183,16 @@ namespace osu.Game.Tests.NonVisual
|
||||
{
|
||||
var osu = LoadOsuIntoHost(host);
|
||||
|
||||
Assert.DoesNotThrow(() => osu.Migrate(customPath));
|
||||
Assert.DoesNotThrow(() => osu.MigrateUserData(customPath));
|
||||
Assert.That(File.Exists(Path.Combine(customPath, OsuGameBase.CLIENT_DATABASE_FILENAME)));
|
||||
|
||||
Assert.DoesNotThrow(() => osu.Migrate(customPath2));
|
||||
Assert.DoesNotThrow(() => osu.MigrateUserData(customPath2));
|
||||
Assert.That(File.Exists(Path.Combine(customPath2, OsuGameBase.CLIENT_DATABASE_FILENAME)));
|
||||
|
||||
// some files may have been left behind for whatever reason, but that's not what we're testing here.
|
||||
cleanupPath(customPath);
|
||||
|
||||
Assert.DoesNotThrow(() => osu.Migrate(customPath));
|
||||
Assert.DoesNotThrow(() => osu.MigrateUserData(customPath));
|
||||
Assert.That(File.Exists(Path.Combine(customPath, OsuGameBase.CLIENT_DATABASE_FILENAME)));
|
||||
}
|
||||
finally
|
||||
@@ -212,8 +212,8 @@ namespace osu.Game.Tests.NonVisual
|
||||
{
|
||||
var osu = LoadOsuIntoHost(host);
|
||||
|
||||
Assert.DoesNotThrow(() => osu.Migrate(customPath));
|
||||
Assert.Throws<ArgumentException>(() => osu.Migrate(customPath));
|
||||
Assert.DoesNotThrow(() => osu.MigrateUserData(customPath));
|
||||
Assert.Throws<ArgumentException>(() => osu.MigrateUserData(customPath));
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -238,14 +238,14 @@ namespace osu.Game.Tests.NonVisual
|
||||
|
||||
string originalDirectory = storage.GetFullPath(".");
|
||||
|
||||
Assert.DoesNotThrow(() => osu.Migrate(customPath));
|
||||
Assert.DoesNotThrow(() => osu.MigrateUserData(customPath));
|
||||
Assert.That(File.Exists(Path.Combine(customPath, OsuGameBase.CLIENT_DATABASE_FILENAME)));
|
||||
|
||||
Directory.CreateDirectory(customPath2);
|
||||
File.WriteAllText(Path.Combine(customPath2, OsuGameBase.CLIENT_DATABASE_FILENAME), "I am a text");
|
||||
|
||||
// Fails because file already exists.
|
||||
Assert.Throws<ArgumentException>(() => osu.Migrate(customPath2));
|
||||
Assert.Throws<ArgumentException>(() => osu.MigrateUserData(customPath2));
|
||||
|
||||
osuStorage?.ChangeDataPath(customPath2);
|
||||
|
||||
@@ -269,7 +269,7 @@ namespace osu.Game.Tests.NonVisual
|
||||
{
|
||||
var osu = LoadOsuIntoHost(host);
|
||||
|
||||
Assert.DoesNotThrow(() => osu.Migrate(customPath));
|
||||
Assert.DoesNotThrow(() => osu.MigrateUserData(customPath));
|
||||
|
||||
string subFolder = Path.Combine(customPath, "sub");
|
||||
|
||||
@@ -278,7 +278,7 @@ namespace osu.Game.Tests.NonVisual
|
||||
|
||||
Directory.CreateDirectory(subFolder);
|
||||
|
||||
Assert.Throws<ArgumentException>(() => osu.Migrate(subFolder));
|
||||
Assert.Throws<ArgumentException>(() => osu.MigrateUserData(subFolder));
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -297,7 +297,7 @@ namespace osu.Game.Tests.NonVisual
|
||||
{
|
||||
var osu = LoadOsuIntoHost(host);
|
||||
|
||||
Assert.DoesNotThrow(() => osu.Migrate(customPath));
|
||||
Assert.DoesNotThrow(() => osu.MigrateUserData(customPath));
|
||||
|
||||
string seeminglySubFolder = customPath + "sub";
|
||||
|
||||
@@ -306,7 +306,7 @@ namespace osu.Game.Tests.NonVisual
|
||||
|
||||
Directory.CreateDirectory(seeminglySubFolder);
|
||||
|
||||
osu.Migrate(seeminglySubFolder);
|
||||
osu.MigrateUserData(seeminglySubFolder);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
[
|
||||
{"beatmapset_id":2529695,"difficulty_rating":7.61701,"id":5590715,"mode":"mania","status":"ranked","total_length":396,"user_id":13371424,"version":"[4K] Memoriae Effervescentes","accuracy":7.2,"ar":5,"bpm":220,"convert":false,"count_circles":2097,"count_sliders":3556,"count_spinners":0,"cs":4,"deleted_at":null,"drain":8.2,"hit_length":395,"is_scoreable":true,"last_updated":"2026-04-21T08:35:33Z","mode_int":3,"passcount":1485,"playcount":4006,"ranked":1,"url":"https:\/\/osu.ppy.sh\/beatmaps\/5590715","checksum":"5b43b30845408f6bf0cc02d19fa475a4","beatmapset":{"anime_cover":false,"artist":"Laur","artist_unicode":"Laur","covers":{"cover":"https:\/\/assets.ppy.sh\/beatmaps\/2529695\/covers\/cover.jpg?1776760548","cover@2x":"https:\/\/assets.ppy.sh\/beatmaps\/2529695\/covers\/cover@2x.jpg?1776760548","card":"https:\/\/assets.ppy.sh\/beatmaps\/2529695\/covers\/card.jpg?1776760548","card@2x":"https:\/\/assets.ppy.sh\/beatmaps\/2529695\/covers\/card@2x.jpg?1776760548","list":"https:\/\/assets.ppy.sh\/beatmaps\/2529695\/covers\/list.jpg?1776760548","list@2x":"https:\/\/assets.ppy.sh\/beatmaps\/2529695\/covers\/list@2x.jpg?1776760548","slimcover":"https:\/\/assets.ppy.sh\/beatmaps\/2529695\/covers\/slimcover.jpg?1776760548","slimcover@2x":"https:\/\/assets.ppy.sh\/beatmaps\/2529695\/covers\/slimcover@2x.jpg?1776760548"},"creator":"Ainer","favourite_count":55,"genre_id":10,"hype":null,"id":2529695,"language_id":5,"nsfw":false,"offset":0,"play_count":4102,"preview_url":"https:\/\/b.ppy.sh\/preview\/2529695.mp3","source":"osu!mania 7K World Cup 2026","spotlight":false,"status":"ranked","title":"SEV-26","title_unicode":"SEV-26","track_id":11695,"user_id":13371424,"video":false,"bpm":180,"can_be_hyped":false,"deleted_at":null,"discussion_enabled":true,"discussion_locked":false,"is_scoreable":true,"last_updated":"2026-04-21T08:35:32Z","legacy_thread_url":"https:\/\/osu.ppy.sh\/community\/forums\/topics\/2191856","nominations_summary":{"current":2,"eligible_main_rulesets":["mania"],"required_meta":{"main_ruleset":2,"non_main_ruleset":1}},"ranked":1,"ranked_date":"2026-04-28T09:22:15Z","rating":8.96296,"storyboard":false,"submitted_date":"2026-03-28T00:58:23Z","tags":"featured artist fa mappers' guild mg mpg osu! original electronic instrumental neurofunk hardcore psytrance speedcore tearout dubstep orchestral artcore sev26 sylvatic encephalitis virus grand finals grandfinals gf mwc 2026 world cup mwc2026 mwc7k2026 tb tiebreaker alicia antipole symmatrix- sardines bruh_shen tehfire polytetral hourius naraicat alexdunk lowgraphics sakura006","availability":{"download_disabled":false,"more_information":null},"ratings":[0,3,0,0,0,0,0,0,0,1,23]},"current_user_playcount":0,"failtimes":{"fail":[0,0,0,0,0,0,0,0,9,0,9,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,0,18,18,0,0,0,0,0,0,0,0,9,9,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,18,0,0,18,9,9,18,0,18,9,0,0,0,0,0],"exit":[0,36,36,45,63,9,9,54,18,63,72,18,9,0,9,9,0,18,27,18,18,45,0,9,9,9,9,27,9,9,9,9,9,0,0,9,0,9,0,0,0,0,0,0,18,0,0,0,0,9,0,9,0,0,0,0,18,18,9,9,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,18,0,0,9,9,0,0,9,18,0,9,0,0,0,0,0,0,0]},"max_combo":9163,"owners":[{"id":13371424,"username":"Ainer"},{"id":17258072,"username":"Alicia"}]},
|
||||
{"beatmapset_id":2518293,"difficulty_rating":3.61767,"id":5602450,"mode":"mania","status":"ranked","total_length":282,"user_id":8425052,"version":"[4K] Innocence","accuracy":8,"ar":5,"bpm":140,"convert":false,"count_circles":3618,"count_sliders":0,"count_spinners":0,"cs":4,"deleted_at":null,"drain":8,"hit_length":281,"is_scoreable":true,"last_updated":"2026-04-13T00:00:32Z","mode_int":3,"passcount":369,"playcount":1963,"ranked":1,"url":"https:\/\/osu.ppy.sh\/beatmaps\/5602450","checksum":"51319d68b9373eb98807ea5a2187d803","beatmapset":{"anime_cover":false,"artist":"rejection","artist_unicode":"rejection","covers":{"cover":"https:\/\/assets.ppy.sh\/beatmaps\/2518293\/covers\/cover.jpg?1776038446","cover@2x":"https:\/\/assets.ppy.sh\/beatmaps\/2518293\/covers\/cover@2x.jpg?1776038446","card":"https:\/\/assets.ppy.sh\/beatmaps\/2518293\/covers\/card.jpg?1776038446","card@2x":"https:\/\/assets.ppy.sh\/beatmaps\/2518293\/covers\/card@2x.jpg?1776038446","list":"https:\/\/assets.ppy.sh\/beatmaps\/2518293\/covers\/list.jpg?1776038446","list@2x":"https:\/\/assets.ppy.sh\/beatmaps\/2518293\/covers\/list@2x.jpg?1776038446","slimcover":"https:\/\/assets.ppy.sh\/beatmaps\/2518293\/covers\/slimcover.jpg?1776038446","slimcover@2x":"https:\/\/assets.ppy.sh\/beatmaps\/2518293\/covers\/slimcover@2x.jpg?1776038446"},"creator":"Stella-","favourite_count":48,"genre_id":10,"hype":null,"id":2518293,"language_id":3,"nsfw":false,"offset":0,"play_count":2031,"preview_url":"https:\/\/b.ppy.sh\/preview\/2518293.mp3","source":"","spotlight":false,"status":"ranked","title":"White Canvas (feat. Aitsuki Nakuru)","title_unicode":"White Canvas (feat. \u85cd\u6708\u306a\u304f\u308b)","track_id":5057,"user_id":8425052,"video":false,"bpm":140,"can_be_hyped":false,"deleted_at":null,"discussion_enabled":true,"discussion_locked":false,"is_scoreable":true,"last_updated":"2026-04-13T00:00:32Z","legacy_thread_url":"https:\/\/osu.ppy.sh\/community\/forums\/topics\/2185354","nominations_summary":{"current":2,"eligible_main_rulesets":["mania"],"required_meta":{"main_ruleset":2,"non_main_ruleset":1}},"ranked":1,"ranked_date":"2026-04-21T23:42:39Z","rating":8.66667,"storyboard":false,"submitted_date":"2026-03-06T10:45:47Z","tags":"fa featured artist japanese pop jpop j-pop electronic encore emotional -emotional vocal pop 02- 2 megarex mrx-074 mrx074 female vocals vocalist m3-2020\u79cb fall m3-46 muse dash the future","availability":{"download_disabled":false,"more_information":null},"ratings":[0,0,0,0,0,1,0,1,0,0,4]},"current_user_playcount":0,"failtimes":{"fail":[0,0,0,0,9,0,9,0,0,9,0,0,0,0,0,0,0,0,0,0,0,9,0,9,0,0,0,9,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"exit":[0,0,18,18,9,63,9,27,18,9,18,9,18,9,27,9,36,0,0,9,9,0,9,0,18,27,0,0,0,9,9,0,0,0,0,9,0,9,0,9,9,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,9,0,0,0,9,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},"max_combo":3618,"owners":[{"id":8425052,"username":"Stella-"}]},
|
||||
{"beatmapset_id":1665948,"difficulty_rating":4.87529,"id":3871759,"mode":"mania","status":"ranked","total_length":379,"user_id":17272017,"version":"[4K] Time Freeze Illusion","accuracy":8.5,"ar":5,"bpm":200,"convert":false,"count_circles":5264,"count_sliders":629,"count_spinners":0,"cs":4,"deleted_at":null,"drain":8,"hit_length":377,"is_scoreable":true,"last_updated":"2026-04-13T09:52:53Z","mode_int":3,"passcount":308,"playcount":1738,"ranked":1,"url":"https:\/\/osu.ppy.sh\/beatmaps\/3871759","checksum":"34b98351e32ca3db24ea9fc0055a923b","beatmapset":{"anime_cover":true,"artist":"Release Hallucination","artist_unicode":"Release Hallucination","covers":{"cover":"https:\/\/assets.ppy.sh\/beatmaps\/1665948\/covers\/cover.jpg?1776073987","cover@2x":"https:\/\/assets.ppy.sh\/beatmaps\/1665948\/covers\/cover@2x.jpg?1776073987","card":"https:\/\/assets.ppy.sh\/beatmaps\/1665948\/covers\/card.jpg?1776073987","card@2x":"https:\/\/assets.ppy.sh\/beatmaps\/1665948\/covers\/card@2x.jpg?1776073987","list":"https:\/\/assets.ppy.sh\/beatmaps\/1665948\/covers\/list.jpg?1776073987","list@2x":"https:\/\/assets.ppy.sh\/beatmaps\/1665948\/covers\/list@2x.jpg?1776073987","slimcover":"https:\/\/assets.ppy.sh\/beatmaps\/1665948\/covers\/slimcover.jpg?1776073987","slimcover@2x":"https:\/\/assets.ppy.sh\/beatmaps\/1665948\/covers\/slimcover@2x.jpg?1776073987"},"creator":"Lazurent","favourite_count":51,"genre_id":11,"hype":null,"id":1665948,"language_id":3,"nsfw":false,"offset":0,"play_count":4811,"preview_url":"https:\/\/b.ppy.sh\/preview\/1665948.mp3","source":"","spotlight":false,"status":"ranked","title":"Chronostasis","title_unicode":"Chronostasis","track_id":4954,"user_id":17272017,"video":false,"bpm":200,"can_be_hyped":false,"deleted_at":null,"discussion_enabled":true,"discussion_locked":false,"is_scoreable":true,"last_updated":"2026-04-13T09:52:53Z","legacy_thread_url":"https:\/\/osu.ppy.sh\/community\/forums\/topics\/1494910","nominations_summary":{"current":2,"eligible_main_rulesets":["mania"],"required_meta":{"main_ruleset":2,"non_main_ruleset":1}},"ranked":1,"ranked_date":"2026-04-20T11:25:45Z","rating":8.33333,"storyboard":false,"submitted_date":"2022-01-03T14:00:15Z","tags":"m3-38 symphonic progressive metal marathon emi gothic kaorin kaoru hirato japanese \u30af\u30ed\u30ce\u30b9\u30bf\u30b7\u30b9 fa featured artist mg mpg mappers' guild l43yrnt","availability":{"download_disabled":false,"more_information":null},"ratings":[0,1,0,0,0,0,0,0,0,1,4]},"current_user_playcount":0,"failtimes":{"fail":[0,0,0,0,9,9,63,18,45,18,45,27,0,0,0,0,0,9,0,0,0,0,9,18,0,0,0,9,9,0,0,0,0,0,18,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,0,18,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,0],"exit":[0,0,18,63,63,135,261,216,126,81,81,36,18,27,45,27,27,36,9,9,0,18,18,81,27,18,9,18,18,9,0,0,0,36,45,36,0,0,0,0,0,0,9,9,0,18,0,0,9,0,9,0,9,0,0,9,0,9,9,0,0,9,0,0,0,9,0,0,0,0,0,0,0,0,0,9,9,0,0,0,0,0,9,0,0,0,0,0,0,0,0,9,0,0,9,0,0,0,0,9]},"max_combo":6974,"owners":[{"id":17272017,"username":"Lazurent"}]},
|
||||
{"beatmapset_id":2535368,"difficulty_rating":5.82018,"id":5606802,"mode":"mania","status":"ranked","total_length":296,"user_id":10085090,"version":"[4K] Desperate Soul","accuracy":8.5,"ar":5,"bpm":240,"convert":false,"count_circles":6288,"count_sliders":0,"count_spinners":0,"cs":4,"deleted_at":null,"drain":8,"hit_length":290,"is_scoreable":true,"last_updated":"2026-04-16T18:03:43Z","mode_int":3,"passcount":208,"playcount":1738,"ranked":1,"url":"https:\/\/osu.ppy.sh\/beatmaps\/5606802","checksum":"55451c2f43654e4ec7b9bb156b93148e","beatmapset":{"anime_cover":false,"artist":"Imperial Circus Dead Decadence","artist_unicode":"Imperial Circus Dead Decadence","covers":{"cover":"https:\/\/assets.ppy.sh\/beatmaps\/2535368\/covers\/cover.jpg?1776362637","cover@2x":"https:\/\/assets.ppy.sh\/beatmaps\/2535368\/covers\/cover@2x.jpg?1776362637","card":"https:\/\/assets.ppy.sh\/beatmaps\/2535368\/covers\/card.jpg?1776362637","card@2x":"https:\/\/assets.ppy.sh\/beatmaps\/2535368\/covers\/card@2x.jpg?1776362637","list":"https:\/\/assets.ppy.sh\/beatmaps\/2535368\/covers\/list.jpg?1776362637","list@2x":"https:\/\/assets.ppy.sh\/beatmaps\/2535368\/covers\/list@2x.jpg?1776362637","slimcover":"https:\/\/assets.ppy.sh\/beatmaps\/2535368\/covers\/slimcover.jpg?1776362637","slimcover@2x":"https:\/\/assets.ppy.sh\/beatmaps\/2535368\/covers\/slimcover@2x.jpg?1776362637"},"creator":"Carpihat","favourite_count":48,"genre_id":11,"hype":null,"id":2535368,"language_id":3,"nsfw":false,"offset":0,"play_count":1961,"preview_url":"https:\/\/b.ppy.sh\/preview\/2535368.mp3","source":"","spotlight":false,"status":"ranked","title":"Jashin no Konrei, Gi wa Ai to Shiru.","title_unicode":"\u90aa\u795e\u306e\u5a5a\u793c\u3001\u5100\u306f\u611b\u3068\u77e5\u308b\u3002","track_id":862,"user_id":10085090,"video":false,"bpm":240,"can_be_hyped":false,"deleted_at":null,"discussion_enabled":true,"discussion_locked":false,"is_scoreable":true,"last_updated":"2026-04-16T18:03:42Z","legacy_thread_url":"https:\/\/osu.ppy.sh\/community\/forums\/topics\/2195210","nominations_summary":{"current":2,"eligible_main_rulesets":["mania"],"required_meta":{"main_ruleset":2,"non_main_ruleset":1}},"ranked":1,"ranked_date":"2026-04-18T18:05:56Z","rating":10,"storyboard":false,"submitted_date":"2026-04-07T09:36:37Z","tags":"cthulhu wedding blackened melodic symphonic death black metal melodeath icdd \u72c2\u304a\u3057\u304f\u54b2\u3044\u305f\u51c4\u60e8\u306a\u9ab8\u306f\u594f\u3067\u3001\u611b\u304a\u3057\u304f\u88c2\u3044\u305f\u5c11\u5973\u306f\u8056\u9910\u306e\u8a5e\u3092\u8b33\u3046\u3002 kurooshiku saita seisan na mukuro wa kanaderu itooshiku shoujo seisen no kotoba wo utau fa featured artist japanese mpg mg mappers' guild","availability":{"download_disabled":false,"more_information":null},"ratings":[0,0,0,0,0,0,0,0,0,0,1]},"current_user_playcount":0,"failtimes":{"fail":[0,0,0,9,27,27,27,9,9,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"exit":[0,0,0,27,63,117,108,36,63,36,9,45,9,27,9,0,9,9,0,36,9,36,0,9,0,9,9,0,0,0,27,9,9,0,0,0,0,0,9,0,9,27,9,0,9,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},"max_combo":6288,"owners":[{"id":10085090,"username":"Carpihat"}]},
|
||||
{"beatmapset_id":2348607,"difficulty_rating":5.57212,"id":5140134,"mode":"mania","status":"ranked","total_length":263,"user_id":23384715,"version":"[4K] Enraged Apparition","accuracy":8,"ar":10,"bpm":250,"convert":false,"count_circles":4682,"count_sliders":120,"count_spinners":0,"cs":4,"deleted_at":null,"drain":8,"hit_length":251,"is_scoreable":true,"last_updated":"2026-03-26T20:54:51Z","mode_int":3,"passcount":404,"playcount":2674,"ranked":1,"url":"https:\/\/osu.ppy.sh\/beatmaps\/5140134","checksum":"a2dd0c1b8aa0d3396b47621b758c6adf","beatmapset":{"anime_cover":true,"artist":"Imperial Circus Dead Decadence","artist_unicode":"Imperial Circus Dead Decadence","covers":{"cover":"https:\/\/assets.ppy.sh\/beatmaps\/2348607\/covers\/cover.jpg?1774558514","cover@2x":"https:\/\/assets.ppy.sh\/beatmaps\/2348607\/covers\/cover@2x.jpg?1774558514","card":"https:\/\/assets.ppy.sh\/beatmaps\/2348607\/covers\/card.jpg?1774558514","card@2x":"https:\/\/assets.ppy.sh\/beatmaps\/2348607\/covers\/card@2x.jpg?1774558514","list":"https:\/\/assets.ppy.sh\/beatmaps\/2348607\/covers\/list.jpg?1774558514","list@2x":"https:\/\/assets.ppy.sh\/beatmaps\/2348607\/covers\/list@2x.jpg?1774558514","slimcover":"https:\/\/assets.ppy.sh\/beatmaps\/2348607\/covers\/slimcover.jpg?1774558514","slimcover@2x":"https:\/\/assets.ppy.sh\/beatmaps\/2348607\/covers\/slimcover@2x.jpg?1774558514"},"creator":"Sayuka","favourite_count":435,"genre_id":11,"hype":null,"id":2348607,"language_id":3,"nsfw":false,"offset":0,"play_count":118018,"preview_url":"https:\/\/b.ppy.sh\/preview\/2348607.mp3","source":"","spotlight":false,"status":"ranked","title":"Shinbatsu o Tadori Kyoukotsu ni Itaru","title_unicode":"\u795e\u7f70\u3092\u8fbf\u308a\u72c2\u9aa8\u306b\u81f3\u308b","track_id":8223,"user_id":11322604,"video":false,"bpm":250,"can_be_hyped":false,"deleted_at":null,"discussion_enabled":true,"discussion_locked":false,"is_scoreable":true,"last_updated":"2026-03-26T20:54:46Z","legacy_thread_url":"https:\/\/osu.ppy.sh\/community\/forums\/topics\/2061550","nominations_summary":{"current":3,"eligible_main_rulesets":["osu"],"required_meta":{"main_ruleset":2,"non_main_ruleset":1}},"ranked":1,"ranked_date":"2026-04-12T17:43:03Z","rating":9.11539,"storyboard":false,"submitted_date":"2025-04-02T11:30:44Z","tags":"maaadbot rhythmnoodles ciiyus icdd sinyus20 oomf chan ciyus miapah fort danilmaz1 mekadon -aly arthro roupus julie maiev worthlessnut9 frawog mithew m1ts shoyeu take amats sprixx chocomilk \u9ec4\u6cc9\u3088\u308a\u8074\u3053\u3086\u3001\u7687\u56fd\u306e\u71c8\u3068\u7114\u306e\u5c11\u5973\u3002 japanese male vocals symphonic melodic black death metal doujin hull kim rib:y(uhki) rib yuhki shuhei js jumpstream hs handstream stream stamina jack chordjack rice fa featured artist","availability":{"download_disabled":false,"more_information":null},"ratings":[0,7,0,0,1,0,1,1,3,10,81]},"current_user_playcount":0,"failtimes":{"fail":[0,0,0,18,72,54,36,36,18,9,9,9,0,0,0,9,63,18,0,0,0,0,0,0,9,0,0,0,9,9,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,18,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0],"exit":[0,0,9,243,90,198,216,126,18,81,63,36,9,0,18,135,45,27,27,18,18,9,0,0,0,0,9,9,18,27,36,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,27,18,9,0,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,9,0,27,0,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,9,9,0]},"max_combo":5113,"owners":[{"id":16018038,"username":"frawog"},{"id":23384715,"username":"Worthlessnut9"}]}
|
||||
|
||||
]
|
||||
@@ -0,0 +1,84 @@
|
||||
// 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.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Tests.Rulesets.Scoring
|
||||
{
|
||||
public class ScoreMultiplierCalculatorTest
|
||||
{
|
||||
[Test]
|
||||
public void TestFlatMultiplier()
|
||||
{
|
||||
var calculator = new TestScoreMultiplierCalculator();
|
||||
|
||||
double multiplier = calculator.CalculateFor([new OsuModEasy()]);
|
||||
|
||||
Assert.That(multiplier, Is.EqualTo(0.15));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSettingDependentMultiplier()
|
||||
{
|
||||
var calculator = new TestScoreMultiplierCalculator();
|
||||
|
||||
double multiplier = calculator.CalculateFor([new OsuModDaycore { SpeedChange = { Value = 0.6 } }]);
|
||||
|
||||
Assert.That(multiplier, Is.EqualTo(0.4));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestContextDependentMultiplier()
|
||||
{
|
||||
var calculator = new TestScoreMultiplierCalculator();
|
||||
|
||||
double multiplier;
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
calculator.HardRockPenalty = false;
|
||||
multiplier = calculator.CalculateFor([new OsuModHardRock()]);
|
||||
Assert.That(multiplier, Is.EqualTo(1.4));
|
||||
|
||||
calculator.HardRockPenalty = true;
|
||||
multiplier = calculator.CalculateFor([new OsuModHardRock()]);
|
||||
Assert.That(multiplier, Is.EqualTo(1.2));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCombinationMultiplier()
|
||||
{
|
||||
var calculator = new TestScoreMultiplierCalculator();
|
||||
|
||||
double multiplier = calculator.CalculateFor([new OsuModEasy(), new OsuModDaycore()]);
|
||||
|
||||
Assert.That(multiplier, Is.EqualTo(0.003));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCombinationAndFlatMultipliers()
|
||||
{
|
||||
var calculator = new TestScoreMultiplierCalculator();
|
||||
|
||||
double multiplier = calculator.CalculateFor([new OsuModDaycore(), new OsuModHardRock(), new OsuModEasy()]);
|
||||
|
||||
Assert.That(multiplier, Is.EqualTo(0.003 * 1.4));
|
||||
}
|
||||
|
||||
private class TestScoreMultiplierCalculator : ScoreMultiplierCalculator
|
||||
{
|
||||
static TestScoreMultiplierCalculator()
|
||||
{
|
||||
Single<OsuModEasy>(hasMultiplier: 0.15);
|
||||
Single<OsuModDaycore>(hasMultiplier: daycore => (1 + daycore.SpeedChange.Value) / 4);
|
||||
Single<OsuModHardRock, TestScoreMultiplierCalculator>(hasMultiplier: (_, ctx) => ctx.HardRockPenalty ? 1.2 : 1.4);
|
||||
Combination<OsuModEasy, OsuModDaycore>(hasMultiplier: (_, _) => 0.003);
|
||||
}
|
||||
|
||||
public bool HardRockPenalty { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
@@ -26,17 +25,15 @@ namespace osu.Game.Tests.Skins
|
||||
public partial class TestSceneBeatmapSkinLookupDisables : OsuTestScene
|
||||
{
|
||||
private UserSkinSource userSource;
|
||||
private BeatmapSkinProvidingContainer beatmapSkinProvider;
|
||||
private BeatmapSkinSource beatmapSource;
|
||||
private SkinRequester requester;
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; }
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
Add(new SkinProvidingContainer(userSource = new UserSkinSource())
|
||||
.WithChild(new BeatmapSkinProvidingContainer(beatmapSource = new BeatmapSkinSource())
|
||||
.WithChild(beatmapSkinProvider = new BeatmapSkinProvidingContainer(beatmapSource = new BeatmapSkinSource())
|
||||
.WithChild(requester = new SkinRequester())));
|
||||
});
|
||||
|
||||
@@ -44,7 +41,7 @@ namespace osu.Game.Tests.Skins
|
||||
[TestCase(true)]
|
||||
public void TestDrawableLookup(bool allowBeatmapLookups)
|
||||
{
|
||||
AddStep($"Set beatmap skin enabled to {allowBeatmapLookups}", () => config.SetValue(OsuSetting.BeatmapSkins, allowBeatmapLookups));
|
||||
AddStep($"Set beatmap skin enabled to {allowBeatmapLookups}", () => beatmapSkinProvider.BeatmapSkins.Value = allowBeatmapLookups);
|
||||
|
||||
string expected = allowBeatmapLookups ? "beatmap" : "user";
|
||||
|
||||
@@ -55,7 +52,7 @@ namespace osu.Game.Tests.Skins
|
||||
[TestCase(true)]
|
||||
public void TestProviderLookup(bool allowBeatmapLookups)
|
||||
{
|
||||
AddStep($"Set beatmap skin enabled to {allowBeatmapLookups}", () => config.SetValue(OsuSetting.BeatmapSkins, allowBeatmapLookups));
|
||||
AddStep($"Set beatmap skin enabled to {allowBeatmapLookups}", () => beatmapSkinProvider.BeatmapSkins.Value = allowBeatmapLookups);
|
||||
|
||||
ISkin expected() => allowBeatmapLookups ? beatmapSource : userSource;
|
||||
|
||||
|
||||
@@ -355,6 +355,7 @@ namespace osu.Game.Tests.Visual.Background
|
||||
public double StartTime => double.MinValue;
|
||||
public double EndTime => double.MaxValue;
|
||||
public double EndTimeForDisplay => double.MaxValue;
|
||||
public StoryboardElementSource Source => StoryboardElementSource.Beatmap;
|
||||
|
||||
public Drawable CreateDrawable() => new DrawableTestStoryboardElement();
|
||||
}
|
||||
|
||||
@@ -100,8 +100,10 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
[Test]
|
||||
public void TestPlacementOfConcurrentObjectWithDuration()
|
||||
{
|
||||
AddStep("seek to timing point", () => EditorClock.Seek(2170));
|
||||
AddStep("add hit circle", () => EditorBeatmap.Add(createHitCircle(2170, Vector2.Zero)));
|
||||
const double spinner_start_time = 2170;
|
||||
const double spinner_end_seek_time = 2500;
|
||||
|
||||
AddStep("seek to timing point", () => EditorClock.Seek(spinner_start_time));
|
||||
|
||||
AddStep("choose spinner placement tool", () =>
|
||||
{
|
||||
@@ -116,10 +118,15 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
});
|
||||
AddStep("end placing spinner", () =>
|
||||
{
|
||||
EditorClock.Seek(2500);
|
||||
EditorClock.Seek(spinner_end_seek_time);
|
||||
InputManager.Click(MouseButton.Right);
|
||||
});
|
||||
|
||||
AddStep("add hit circle mid-spinner", () =>
|
||||
{
|
||||
EditorBeatmap.Add(createHitCircle((spinner_start_time + spinner_end_seek_time) / 2, Vector2.Zero));
|
||||
});
|
||||
|
||||
AddAssert("two timeline blueprints present", () => Editor.ChildrenOfType<TimelineHitObjectBlueprint>().Count() == 2);
|
||||
}
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
});
|
||||
|
||||
AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint()));
|
||||
AddStep("select circle", () => hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "HitCircle").TriggerClick());
|
||||
AddStep("select circle", () => hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "Hit circle").TriggerClick());
|
||||
AddStep("move mouse to compose", () => InputManager.MoveMouseTo(hitObjectComposer.ChildrenOfType<HitObjectContainer>().Single()));
|
||||
AddStep("click", () => InputManager.Click(MouseButton.Left));
|
||||
AddAssert("circle placed", () => editorBeatmap.HitObjects.Count == 1);
|
||||
@@ -128,10 +128,10 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
AddStep("clear all control points", () => editorBeatmap.ControlPointInfo.Clear());
|
||||
|
||||
AddAssert("Tool is selection", () => hitObjectComposer.ChildrenOfType<ComposeBlueprintContainer>().First().CurrentTool is SelectTool);
|
||||
AddAssert("Hitcircle button not clickable", () => !hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "HitCircle").Enabled.Value);
|
||||
AddAssert("Hitcircle button not clickable", () => !hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "Hit circle").Enabled.Value);
|
||||
AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint()));
|
||||
AddAssert("Hitcircle button is clickable", () => hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "HitCircle").Enabled.Value);
|
||||
AddStep("Change to hitcircle", () => hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "HitCircle").TriggerClick());
|
||||
AddAssert("Hitcircle button is clickable", () => hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "Hit circle").Enabled.Value);
|
||||
AddStep("Change to hitcircle", () => hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "Hit circle").TriggerClick());
|
||||
AddAssert("Tool changed", () => hitObjectComposer.ChildrenOfType<ComposeBlueprintContainer>().First().CurrentTool is HitCircleCompositionTool);
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint()));
|
||||
|
||||
AddStep("Change to hitcircle", () => hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "HitCircle").TriggerClick());
|
||||
AddStep("Change to hitcircle", () => hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "Hit circle").TriggerClick());
|
||||
|
||||
ExpandingToolboxContainer toolboxContainer = null!;
|
||||
|
||||
@@ -186,7 +186,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint()));
|
||||
|
||||
AddStep("Change to hitcircle", () => hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "HitCircle").TriggerClick());
|
||||
AddStep("Change to hitcircle", () => hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "Hit circle").TriggerClick());
|
||||
|
||||
AddStep("move mouse to scroll area", () =>
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
@@ -12,10 +13,11 @@ using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Edit.Components.TernaryButtons;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
@@ -51,6 +53,90 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
AddAssert("circle removed", () => EditorBeatmap.HitObjects, () => Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlacementReplacesObjectAtSameStartTime()
|
||||
{
|
||||
HitCircle existing = null!;
|
||||
var existingPosition = new Vector2(128, 160);
|
||||
var replacementPosition = new Vector2(400, 280);
|
||||
Playfield playfield = null!;
|
||||
|
||||
AddStep("add existing circle", () =>
|
||||
{
|
||||
EditorBeatmap.Add(existing = new HitCircle
|
||||
{
|
||||
StartTime = 500,
|
||||
Position = existingPosition,
|
||||
});
|
||||
});
|
||||
|
||||
AddStep("seek to same time", () => EditorClock.Seek(500));
|
||||
AddStep("select circle placement tool", () => InputManager.Key(Key.Number2));
|
||||
AddStep("grab playfield", () => playfield = this.ChildrenOfType<Playfield>().Single());
|
||||
AddStep("move mouse to replacement coordinates", () => InputManager.MoveMouseTo(playfield.GamefieldToScreenSpace(replacementPosition)));
|
||||
AddStep("place circle", () => InputManager.Click(MouseButton.Left));
|
||||
|
||||
AddAssert("only one hit object", () => EditorBeatmap.HitObjects.Count, () => Is.EqualTo(1));
|
||||
AddAssert("original instance removed from beatmap", () => EditorBeatmap.HitObjects.Single(), () => Is.Not.SameAs(existing));
|
||||
AddAssert("start time unchanged", () => Precision.AlmostEquals(EditorBeatmap.HitObjects.Single().StartTime, 500));
|
||||
AddAssert("circle at new coordinates", () =>
|
||||
{
|
||||
var circle = (HitCircle)EditorBeatmap.HitObjects.Single();
|
||||
return circle != null
|
||||
&& Precision.AlmostEquals(circle.Position.X, replacementPosition.X)
|
||||
&& Precision.AlmostEquals(circle.Position.Y, replacementPosition.Y);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlacementOnSliderBodyDoesNotRemoveSlider()
|
||||
{
|
||||
Slider originalSlider = null!;
|
||||
|
||||
AddStep("add slider", () =>
|
||||
{
|
||||
EditorBeatmap.Add(originalSlider = new Slider
|
||||
{
|
||||
StartTime = 0,
|
||||
Position = new Vector2(256, 192),
|
||||
Path = new SliderPath(new[]
|
||||
{
|
||||
new PathControlPoint(Vector2.Zero),
|
||||
new PathControlPoint(new Vector2(256, 0)),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
AddUntilStep("slider duration resolved", () => originalSlider.EndTime > originalSlider.StartTime + 1);
|
||||
|
||||
double midTime = 0;
|
||||
double endTime = 0;
|
||||
|
||||
AddStep("capture slider times", () =>
|
||||
{
|
||||
midTime = originalSlider.StartTime + originalSlider.Duration / 2;
|
||||
endTime = originalSlider.EndTime;
|
||||
});
|
||||
|
||||
Playfield playfield = null!;
|
||||
|
||||
AddStep("seek to slider mid", () => EditorClock.Seek(midTime));
|
||||
AddStep("select circle placement tool", () => InputManager.Key(Key.Number2));
|
||||
AddStep("grab playfield", () => playfield = this.ChildrenOfType<Playfield>().Single());
|
||||
AddStep("move mouse for mid placement", () => InputManager.MoveMouseTo(playfield.GamefieldToScreenSpace(new Vector2(300, 200))));
|
||||
AddStep("place circle at slider mid time", () => InputManager.Click(MouseButton.Left));
|
||||
|
||||
AddAssert("slider preserved after mid placement", () => EditorBeatmap.HitObjects.Contains(originalSlider));
|
||||
AddAssert("one circle after mid placement", () => EditorBeatmap.HitObjects.Count(h => h is HitCircle), () => Is.EqualTo(1));
|
||||
|
||||
AddStep("seek to slider end", () => EditorClock.Seek(endTime));
|
||||
AddStep("place circle at slider end time", () => InputManager.Click(MouseButton.Left));
|
||||
|
||||
AddAssert("slider still preserved", () => EditorBeatmap.HitObjects.Contains(originalSlider));
|
||||
AddAssert("three hit objects total", () => EditorBeatmap.HitObjects.Count, () => Is.EqualTo(3));
|
||||
AddAssert("two circles placed", () => EditorBeatmap.HitObjects.Count(h => h is HitCircle), () => Is.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTimingLost()
|
||||
{
|
||||
|
||||
@@ -91,7 +91,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
AddRepeatStep("Add judgement", () => applyOneJudgement(HitResult.LargeTickHit), 2);
|
||||
AddAssert("Check value added whilst hidden", () => hiddenCount() == 2);
|
||||
AddStep("Show all judgements", () => counterDisplay.Mode.Value = ArgonJudgementCounterDisplay.DisplayMode.All);
|
||||
AddStep("Show all judgements", () => counterDisplay.Mode.Value = JudgementCounterDisplay.DisplayMode.All);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -156,7 +156,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddStep("create counter", () => Child = counterDisplay = new TestArgonJudgementCounterDisplay());
|
||||
|
||||
AddStep("Set max judgement to hide itself", () => counterDisplay.ShowMaxJudgement.Value = false);
|
||||
AddStep("Show all judgements", () => counterDisplay.Mode.Value = ArgonJudgementCounterDisplay.DisplayMode.All);
|
||||
AddStep("Show all judgements", () => counterDisplay.Mode.Value = JudgementCounterDisplay.DisplayMode.All);
|
||||
AddWaitStep("wait some", 2);
|
||||
AddAssert("Assert max judgement hidden", () => counterDisplay.CounterFlow.ChildrenOfType<ArgonJudgementCounter>().First().Alpha == 0);
|
||||
}
|
||||
@@ -165,7 +165,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
public void TestNoDuplicates()
|
||||
{
|
||||
AddStep("create counter", () => Child = counterDisplay = new TestArgonJudgementCounterDisplay());
|
||||
AddStep("Show all judgements", () => counterDisplay.Mode.Value = ArgonJudgementCounterDisplay.DisplayMode.All);
|
||||
AddStep("Show all judgements", () => counterDisplay.Mode.Value = JudgementCounterDisplay.DisplayMode.All);
|
||||
AddAssert("Check no duplicates",
|
||||
() => counterDisplay.CounterFlow.ChildrenOfType<ArgonJudgementCounter>().Count(),
|
||||
() => Is.EqualTo(counterDisplay.CounterFlow.ChildrenOfType<ArgonJudgementCounter>().Select(c => c.Result.DisplayName).Distinct().Count()));
|
||||
@@ -176,11 +176,11 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
AddStep("create counter", () => Child = counterDisplay = new TestArgonJudgementCounterDisplay());
|
||||
|
||||
AddStep("Show basic judgements", () => counterDisplay.Mode.Value = ArgonJudgementCounterDisplay.DisplayMode.Simple);
|
||||
AddStep("Show basic judgements", () => counterDisplay.Mode.Value = JudgementCounterDisplay.DisplayMode.Simple);
|
||||
AddWaitStep("wait some", 2);
|
||||
AddAssert("Check only basic", () => counterDisplay.CounterFlow.ChildrenOfType<ArgonJudgementCounter>().Last().Alpha == 0);
|
||||
AddStep("Show normal judgements", () => counterDisplay.Mode.Value = ArgonJudgementCounterDisplay.DisplayMode.Normal);
|
||||
AddStep("Show all judgements", () => counterDisplay.Mode.Value = ArgonJudgementCounterDisplay.DisplayMode.All);
|
||||
AddStep("Show normal judgements", () => counterDisplay.Mode.Value = JudgementCounterDisplay.DisplayMode.Normal);
|
||||
AddStep("Show all judgements", () => counterDisplay.Mode.Value = JudgementCounterDisplay.DisplayMode.All);
|
||||
AddWaitStep("wait some", 2);
|
||||
AddAssert("Check all visible", () => counterDisplay.CounterFlow.ChildrenOfType<ArgonJudgementCounter>().Last().Alpha == 1);
|
||||
AddToggleStep("toggle wireframe display", t => counterDisplay.WireframeOpacity.Value = t ? 0.3f : 0);
|
||||
|
||||
@@ -66,7 +66,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
var layer = storyboard.GetLayer("Background");
|
||||
|
||||
var sprite = new StoryboardSprite(lookup_name, Anchor.TopLeft, new Vector2(256, 192));
|
||||
var sprite = new StoryboardSprite(StoryboardElementSource.Beatmap, lookup_name, Anchor.TopLeft, new Vector2(256, 192));
|
||||
sprite.Commands.AddAlpha(Easing.None, 0, 2000, 0, 2);
|
||||
|
||||
layer.Elements.Clear();
|
||||
@@ -121,7 +121,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
var layer = storyboard.GetLayer("Video");
|
||||
|
||||
var sprite = new StoryboardVideo("Videos/test-video.mp4", Time.Current);
|
||||
var sprite = new StoryboardVideo(StoryboardElementSource.Beatmap, "Videos/test-video.mp4", Time.Current);
|
||||
|
||||
if (scaleTransformProvided)
|
||||
{
|
||||
@@ -250,7 +250,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
var layer = storyboard.GetLayer("Background");
|
||||
|
||||
var sprite = new StoryboardSprite(lookupName, origin, initialPosition);
|
||||
var sprite = new StoryboardSprite(StoryboardElementSource.Beatmap, lookupName, origin, initialPosition);
|
||||
var loop = sprite.AddLoopingGroup(Time.Current, 100);
|
||||
loop.AddAlpha(Easing.None, 0, 10000, 1, 1);
|
||||
|
||||
|
||||
@@ -73,6 +73,15 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
iteration++;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDisplayModes()
|
||||
{
|
||||
AddStep("create counter", () => Child = counterDisplay = new TestJudgementCounterDisplay());
|
||||
|
||||
foreach (JudgementCounterDisplay.DisplayMode mode in Enum.GetValues<JudgementCounterDisplay.DisplayMode>())
|
||||
AddStep($"Change mode to {mode}", () => counterDisplay.Mode.Value = mode);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAddJudgementsToCounters()
|
||||
{
|
||||
|
||||
@@ -47,7 +47,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
var storyboard = new Storyboard();
|
||||
|
||||
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
|
||||
var sprite = new StoryboardSprite(StoryboardElementSource.Beatmap, "unknown", Anchor.TopLeft, Vector2.Zero);
|
||||
|
||||
sprite.Commands.AddAlpha(Easing.None, firstStoryboardEvent, firstStoryboardEvent + 500, 0, 1);
|
||||
|
||||
@@ -73,7 +73,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
var storyboard = new Storyboard();
|
||||
|
||||
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
|
||||
var sprite = new StoryboardSprite(StoryboardElementSource.Beatmap, "unknown", Anchor.TopLeft, Vector2.Zero);
|
||||
|
||||
// these should be ignored as we have an alpha visibility blocker proceeding this command.
|
||||
sprite.Commands.AddScale(Easing.None, loop_start_time, -18000, 0, 1);
|
||||
|
||||
@@ -201,7 +201,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
var layer = storyboard.GetLayer("Background");
|
||||
|
||||
var sprite = new StoryboardSprite(lookup_name, Anchor.Centre, new Vector2(320, 240));
|
||||
var sprite = new StoryboardSprite(StoryboardElementSource.Beatmap, lookup_name, Anchor.Centre, new Vector2(320, 240));
|
||||
sprite.Commands.AddScale(Easing.None, 0, clock_limit, 0.5f, 0.5f);
|
||||
sprite.Commands.AddAlpha(Easing.None, 0, clock_limit, 1, 1);
|
||||
addCommands?.Invoke(sprite);
|
||||
|
||||
@@ -38,10 +38,10 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
storyboard = new Storyboard();
|
||||
var backgroundLayer = storyboard.GetLayer("Background");
|
||||
backgroundLayer.Add(new StoryboardSampleInfo("Intro/welcome.mp3", time: -7000, volume: 20));
|
||||
backgroundLayer.Add(new StoryboardSampleInfo("Intro/welcome.mp3", time: -5000, volume: 20));
|
||||
backgroundLayer.Add(new StoryboardSampleInfo("Intro/welcome.mp3", time: 0, volume: 20));
|
||||
backgroundLayer.Add(new StoryboardSampleInfo("Intro/welcome.mp3", time: 2000, volume: 20));
|
||||
backgroundLayer.Add(new StoryboardSampleInfo(StoryboardElementSource.Beatmap, "Intro/welcome.mp3", time: -7000, volume: 20));
|
||||
backgroundLayer.Add(new StoryboardSampleInfo(StoryboardElementSource.Beatmap, "Intro/welcome.mp3", time: -5000, volume: 20));
|
||||
backgroundLayer.Add(new StoryboardSampleInfo(StoryboardElementSource.Beatmap, "Intro/welcome.mp3", time: 0, volume: 20));
|
||||
backgroundLayer.Add(new StoryboardSampleInfo(StoryboardElementSource.Beatmap, "Intro/welcome.mp3", time: 2000, volume: 20));
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
private Storyboard createStoryboard(double startTime)
|
||||
{
|
||||
var storyboard = new Storyboard();
|
||||
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
|
||||
var sprite = new StoryboardSprite(StoryboardElementSource.Beatmap, "unknown", Anchor.TopLeft, Vector2.Zero);
|
||||
sprite.Commands.AddAlpha(Easing.None, startTime, 0, 0, 1);
|
||||
storyboard.GetLayer("Background").Add(sprite);
|
||||
return storyboard;
|
||||
|
||||
@@ -215,7 +215,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
private Storyboard createStoryboard(double duration)
|
||||
{
|
||||
var storyboard = new Storyboard();
|
||||
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
|
||||
var sprite = new StoryboardSprite(StoryboardElementSource.Beatmap, "unknown", Anchor.TopLeft, Vector2.Zero);
|
||||
sprite.Commands.AddAlpha(Easing.None, 0, duration, 1, 0);
|
||||
storyboard.GetLayer("Background").Add(sprite);
|
||||
return storyboard;
|
||||
|
||||
@@ -71,6 +71,22 @@ namespace osu.Game.Tests.Visual.Matchmaking
|
||||
AddStep("change state to in room", () => queueScreen!.SetState(ScreenQueue.MatchmakingScreenState.InRoom));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDelayedRoomScreenPushDoesNotRunIfRoomIsLeftPrematurely()
|
||||
{
|
||||
AddStep("change state to in room then immediately leave room", () =>
|
||||
{
|
||||
queueScreen!.SetState(ScreenQueue.MatchmakingScreenState.InRoom);
|
||||
MultiplayerClient.LeaveRoom();
|
||||
});
|
||||
|
||||
// the queue screen waits 2 seconds between transitioning to `InRoom` state and actually pushing the relevant screen.
|
||||
// if the room goes to `null` in that time, things die very hard.
|
||||
// therefore the wait here is to check that things don't die very hard.
|
||||
// if they do the test will throw an exception and fail.
|
||||
AddWaitStep("wait a little bit", 10);
|
||||
}
|
||||
|
||||
private static double generateCount(double x, double mean, double stdDev, double amplitude)
|
||||
{
|
||||
return amplitude * Math.Exp(-Math.Pow(x - mean, 2) / (2 * Math.Pow(stdDev, 2))) + Random.Shared.Next(300);
|
||||
|
||||
@@ -48,7 +48,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
allowResponseCallback.Wait(10000);
|
||||
allowResponseCallback.Reset();
|
||||
Schedule(() => d?.Invoke("Incorrect password", new InvalidPasswordException()));
|
||||
Schedule(() => d("Incorrect password", new InvalidPasswordException()));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -493,7 +493,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
[Test]
|
||||
public void TestIntroStoryboardElement() => testLeadIn(b =>
|
||||
{
|
||||
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
|
||||
var sprite = new StoryboardSprite(StoryboardElementSource.Beatmap, "unknown", Anchor.TopLeft, Vector2.Zero);
|
||||
sprite.Commands.AddAlpha(Easing.None, -2000, 0, 0, 1);
|
||||
b.Storyboard.GetLayer("Background").Add(sprite);
|
||||
});
|
||||
|
||||
@@ -1206,6 +1206,27 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddUntilStep("style selection screen closed", () => this.ChildrenOfType<MultiplayerMatchFreestyleSelect>().SingleOrDefault()?.IsCurrentScreen() != true);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMaxParticipantsAndSlots()
|
||||
{
|
||||
createRoom(() => new Room
|
||||
{
|
||||
Name = "Test Room",
|
||||
Password = "password",
|
||||
Playlist =
|
||||
[
|
||||
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo)
|
||||
{
|
||||
RulesetID = new OsuRuleset().RulesetInfo.OnlineID
|
||||
}
|
||||
],
|
||||
MaxParticipants = 10
|
||||
});
|
||||
|
||||
AddStep("turn max participants off", () => multiplayerClient.ChangeSettings(maxParticipants: null));
|
||||
AddStep("turn max participants back on", () => multiplayerClient.ChangeSettings(maxParticipants: 8));
|
||||
}
|
||||
|
||||
private void enterGameplay()
|
||||
{
|
||||
pressReadyButton();
|
||||
|
||||
@@ -330,6 +330,33 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddAssert("score multiplier = 1.20", () => this.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(1.2).Within(0.01));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestModSelectOverlayNonDefaultSettings()
|
||||
{
|
||||
AddStep("add playlist item", () =>
|
||||
{
|
||||
room.Playlist =
|
||||
[
|
||||
new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
|
||||
{
|
||||
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
|
||||
RequiredMods =
|
||||
[
|
||||
new APIMod(new OsuModSuddenDeath { FailOnSliderTail = { Value = true } }),
|
||||
],
|
||||
AllowedMods = [],
|
||||
Freestyle = true
|
||||
}
|
||||
];
|
||||
});
|
||||
ClickButtonWhenEnabled<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>();
|
||||
|
||||
AddUntilStep("wait for join", () => RoomJoined);
|
||||
|
||||
ClickButtonWhenEnabled<UserModSelectButton>();
|
||||
AddAssert("sudden death not visible", () => this.ChildrenOfType<MultiplayerUserModSelectOverlay>().Single().ChildrenOfType<ModPanel>().Single(m => m.Mod is ModSuddenDeath).Visible == false);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestChangeSettingsButtonVisibleForHost()
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
@@ -55,6 +56,68 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddAssert("two unique panels", () => this.ChildrenOfType<ParticipantPanel>().Select(p => p.Current.Value).Distinct().Count() == 2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSlots()
|
||||
{
|
||||
setUpList();
|
||||
AddAssert("one unique panel", () => this.ChildrenOfType<ParticipantPanel>().Select(p => p.Current.Value).Distinct().Count() == 1);
|
||||
|
||||
AddStep("add user", () => MultiplayerClient.AddUser(new APIUser
|
||||
{
|
||||
Id = 3,
|
||||
Username = "Second",
|
||||
CoverUrl = TestResources.COVER_IMAGE_3,
|
||||
}));
|
||||
|
||||
AddAssert("two unique panels", () => this.ChildrenOfType<ParticipantPanel>().Select(p => p.Current.Value).Distinct().Count() == 2);
|
||||
|
||||
AddStep("introduce slots", () => MultiplayerClient.ChangeMatchRoomState(new StandardMatchRoomState
|
||||
{
|
||||
Slots = [null, 3, null, null, 1001, null, null]
|
||||
}).WaitSafely());
|
||||
|
||||
AddStep("click first slot", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<ParticipantPanel>().First());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddUntilStep("slots changed", () => ((StandardMatchRoomState)MultiplayerClient.ClientRoom!.MatchState!).Slots,
|
||||
() => Is.EquivalentTo(new int?[] { 1001, 3, null, null, null, null, null }));
|
||||
|
||||
AddStep("click second slot", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<ParticipantPanel>().ElementAt(1));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddUntilStep("slots not changed", () => ((StandardMatchRoomState)MultiplayerClient.ClientRoom!.MatchState!).Slots,
|
||||
() => Is.EquivalentTo(new int?[] { 1001, 3, null, null, null, null, null }));
|
||||
|
||||
AddStep("click last slot", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<ParticipantPanel>().Last());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddUntilStep("slots changed", () => ((StandardMatchRoomState)MultiplayerClient.ClientRoom!.MatchState!).Slots,
|
||||
() => Is.EquivalentTo(new int?[] { null, 3, null, null, null, null, 1001 }));
|
||||
|
||||
AddStep("shuffle slots", () => MultiplayerClient.ChangeMatchRoomState(new StandardMatchRoomState
|
||||
{
|
||||
Slots = [null, null, 1001, null, null, null, 3]
|
||||
}).WaitSafely());
|
||||
AddStep("remove slots", () => MultiplayerClient.ChangeMatchRoomState(new StandardMatchRoomState
|
||||
{
|
||||
Slots = [null, 3, null, 1001]
|
||||
}).WaitSafely());
|
||||
AddStep("add slots", () => MultiplayerClient.ChangeMatchRoomState(new StandardMatchRoomState
|
||||
{
|
||||
Slots = [null, null, 3, null, 1001, null]
|
||||
}).WaitSafely());
|
||||
AddStep("turn off slots", () => MultiplayerClient.ChangeMatchRoomState(new StandardMatchRoomState
|
||||
{
|
||||
Slots = null
|
||||
}).WaitSafely());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAddReferee()
|
||||
{
|
||||
@@ -86,7 +149,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
AddUntilStep("two unique panels", () => this.ChildrenOfType<ParticipantPanel>().Select(p => p.Current.Value).Distinct().Count() == 2);
|
||||
|
||||
AddStep("kick null user", () => this.ChildrenOfType<ParticipantPanel>().Single(p => p.Current.Value.User == null)
|
||||
AddStep("kick null user", () => this.ChildrenOfType<ParticipantPanel>().Single(p => p.Current.Value.User?.User == null)
|
||||
.ChildrenOfType<ParticipantPanel.KickButton>().Single().TriggerClick());
|
||||
|
||||
AddUntilStep("null user kicked", () => MultiplayerClient.ClientRoom.AsNonNull().Users.Count == 1);
|
||||
@@ -111,7 +174,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
AddStep("remove host", () => MultiplayerClient.RemoveUser(API.LocalUser.Value));
|
||||
|
||||
AddAssert("single panel is for second user", () => this.ChildrenOfType<ParticipantPanel>().Single().Current.Value.UserID == secondUser?.Id);
|
||||
AddAssert("single panel is for second user", () => this.ChildrenOfType<ParticipantPanel>().Single().Current.Value.User?.UserID == secondUser?.Id);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -150,7 +213,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
AddRepeatStep("increment progress", () =>
|
||||
{
|
||||
float progress = this.ChildrenOfType<ParticipantPanel>().Single().Current.Value.BeatmapAvailability.DownloadProgress ?? 0;
|
||||
float progress = this.ChildrenOfType<ParticipantPanel>().Single().Current.Value.User?.BeatmapAvailability.DownloadProgress ?? 0;
|
||||
MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(progress + RNG.NextSingle(0.1f)));
|
||||
}, 25);
|
||||
|
||||
@@ -195,16 +258,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
}));
|
||||
|
||||
AddUntilStep("first user crown visible",
|
||||
() => this.ChildrenOfType<ParticipantPanel>().Single(p => p.Current.Value.UserID == 1001).ChildrenOfType<SpriteIcon>().First().Alpha == 1);
|
||||
() => this.ChildrenOfType<ParticipantPanel>().Single(p => p.Current.Value.User?.UserID == 1001).ChildrenOfType<SpriteIcon>().First().Alpha == 1);
|
||||
AddUntilStep("second user crown hidden",
|
||||
() => this.ChildrenOfType<ParticipantPanel>().Single(p => p.Current.Value.UserID == 3).ChildrenOfType<SpriteIcon>().First().Alpha == 0);
|
||||
() => this.ChildrenOfType<ParticipantPanel>().Single(p => p.Current.Value.User?.UserID == 3).ChildrenOfType<SpriteIcon>().First().Alpha == 0);
|
||||
|
||||
AddStep("make second user host", () => MultiplayerClient.TransferHost(3));
|
||||
|
||||
AddUntilStep("first user crown visible",
|
||||
() => this.ChildrenOfType<ParticipantPanel>().Single(p => p.Current.Value.UserID == 1001).ChildrenOfType<SpriteIcon>().First().Alpha == 0);
|
||||
() => this.ChildrenOfType<ParticipantPanel>().Single(p => p.Current.Value.User?.UserID == 1001).ChildrenOfType<SpriteIcon>().First().Alpha == 0);
|
||||
AddUntilStep("second user crown hidden",
|
||||
() => this.ChildrenOfType<ParticipantPanel>().Single(p => p.Current.Value.UserID == 3).ChildrenOfType<SpriteIcon>().First().Alpha == 1);
|
||||
() => this.ChildrenOfType<ParticipantPanel>().Single(p => p.Current.Value.User?.UserID == 3).ChildrenOfType<SpriteIcon>().First().Alpha == 1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -221,8 +284,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddStep("make second user host", () => MultiplayerClient.TransferHost(3));
|
||||
AddAssert("second user above first", () =>
|
||||
{
|
||||
var first = this.ChildrenOfType<ParticipantPanel>().Single(u => u.Current.Value.UserID == 1001);
|
||||
var second = this.ChildrenOfType<ParticipantPanel>().Single(u => u.Current.Value.UserID == 3);
|
||||
var first = this.ChildrenOfType<ParticipantPanel>().Single(u => u.Current.Value.User?.UserID == 1001);
|
||||
var second = this.ChildrenOfType<ParticipantPanel>().Single(u => u.Current.Value.User?.UserID == 3);
|
||||
return second.ScreenSpaceDrawQuad.TopLeft.Y < first.ScreenSpaceDrawQuad.TopLeft.Y;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
AddUntilStep("4 rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 4);
|
||||
|
||||
AddStep("filter one room", () => container.Filter.Value = new FilterCriteria { SearchString = rooms.First().Name });
|
||||
AddStep("filter one room", () => container.Filter.Value = new LoungeFilterCriteria { SearchString = rooms.First().Name });
|
||||
|
||||
AddUntilStep("1 rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 1);
|
||||
|
||||
@@ -160,13 +160,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddStep("add rooms", () => rooms.AddRange(GenerateRooms(3, new CatchRuleset().RulesetInfo)));
|
||||
|
||||
// Todo: What even is this case...?
|
||||
AddStep("set empty filter criteria", () => container.Filter.Value = new FilterCriteria());
|
||||
AddStep("set empty filter criteria", () => container.Filter.Value = new LoungeFilterCriteria());
|
||||
AddUntilStep("5 rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 5);
|
||||
|
||||
AddStep("filter osu! rooms", () => container.Filter.Value = new FilterCriteria { Ruleset = new OsuRuleset().RulesetInfo });
|
||||
AddStep("filter osu! rooms", () => container.Filter.Value = new LoungeFilterCriteria { Ruleset = new OsuRuleset().RulesetInfo });
|
||||
AddUntilStep("2 rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 2);
|
||||
|
||||
AddStep("filter catch rooms", () => container.Filter.Value = new FilterCriteria { Ruleset = new CatchRuleset().RulesetInfo });
|
||||
AddStep("filter catch rooms", () => container.Filter.Value = new LoungeFilterCriteria { Ruleset = new CatchRuleset().RulesetInfo });
|
||||
AddUntilStep("3 rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 3);
|
||||
}
|
||||
|
||||
@@ -183,11 +183,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
AddUntilStep("both rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 2);
|
||||
|
||||
AddStep("filter public rooms", () => container.Filter.Value = new FilterCriteria { Permissions = RoomPermissionsFilter.Public });
|
||||
AddStep("filter public rooms", () => container.Filter.Value = new LoungeFilterCriteria { Permissions = RoomPermissionsFilter.Public });
|
||||
|
||||
AddUntilStep("private room hidden", () => container.DrawableRooms.All(r => !r.Room.HasPassword));
|
||||
|
||||
AddStep("filter private rooms", () => container.Filter.Value = new FilterCriteria { Permissions = RoomPermissionsFilter.Private });
|
||||
AddStep("filter private rooms", () => container.Filter.Value = new LoungeFilterCriteria { Permissions = RoomPermissionsFilter.Private });
|
||||
|
||||
AddUntilStep("public room hidden", () => container.DrawableRooms.All(r => r.Room.HasPassword));
|
||||
}
|
||||
|
||||
@@ -178,6 +178,23 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddAssert("password icon hidden", () => Precision.AlmostEquals(0, panel.ChildrenOfType<RoomPanel.CornerIcon>().First().Alpha));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSetAndUnsetMaxParticipants()
|
||||
{
|
||||
RoomPanel panel = null!;
|
||||
Room room = null!;
|
||||
|
||||
AddStep("create room", () => Child = panel = createLoungeRoom(room = new Room
|
||||
{
|
||||
Name = "A room",
|
||||
Type = MatchType.HeadToHead,
|
||||
}));
|
||||
|
||||
AddUntilStep("wait for panel load", () => panel.ChildrenOfType<DrawableRoomParticipantsList>().Any());
|
||||
AddStep("set max participants", () => room.MaxParticipants = 5);
|
||||
AddStep("unset max participants", () => room.MaxParticipants = null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMultiplayerRooms()
|
||||
{
|
||||
|
||||
@@ -114,6 +114,27 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
&& songSelect.Beatmap.Value is DummyWorkingBeatmap);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEditorRestoresModeOnReload()
|
||||
{
|
||||
prepareBeatmap();
|
||||
openEditor();
|
||||
|
||||
makeMetadataChange(commit: false);
|
||||
|
||||
Editor editor1 = null!;
|
||||
|
||||
AddStep("store current editor", () => editor1 = getEditor());
|
||||
|
||||
AddStep("reload", () => getEditor().SwitchToDifficulty(getEditorBeatmap().BeatmapInfo));
|
||||
AddUntilStep("save dialog displayed", () => Game.ChildrenOfType<DialogOverlay>().SingleOrDefault()?.CurrentDialog is PromptForSaveDialog);
|
||||
AddStep("confirm", () => InputManager.Key(Key.Number1));
|
||||
|
||||
AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
|
||||
AddAssert("editor is new instance", () => getEditor() != editor1);
|
||||
AddAssert("mode is still song setup", () => getEditor().Mode.Value == EditorScreenMode.SongSetup);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestChangeMetadataExitWhileTextboxFocusedPromptsSave()
|
||||
{
|
||||
|
||||
@@ -253,6 +253,11 @@ namespace osu.Game.Tests.Visual.Online
|
||||
InputManager.MoveMouseTo(btn);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddStep("Set reason to other", () =>
|
||||
{
|
||||
var reason = this.ChildrenOfType<OsuEnumDropdown<CommentReportReason>>().Single();
|
||||
reason.Current.Value = CommentReportReason.Other;
|
||||
});
|
||||
AddStep("Try to report", () =>
|
||||
{
|
||||
var btn = this.ChildrenOfType<ReportCommentPopover>().Single().ChildrenOfType<RoundedButton>().Single();
|
||||
@@ -261,12 +266,10 @@ namespace osu.Game.Tests.Visual.Online
|
||||
});
|
||||
AddWaitStep("Wait", 3);
|
||||
AddAssert("Nothing happened", () => this.ChildrenOfType<ReportCommentPopover>().Any());
|
||||
AddStep("Set report data", () =>
|
||||
AddStep("Add comment", () =>
|
||||
{
|
||||
var field = this.ChildrenOfType<ReportCommentPopover>().Single().ChildrenOfType<OsuTextBox>().First();
|
||||
field.Current.Value = report_text;
|
||||
var reason = this.ChildrenOfType<OsuEnumDropdown<CommentReportReason>>().Single();
|
||||
reason.Current.Value = CommentReportReason.Other;
|
||||
});
|
||||
AddStep("Try to report", () =>
|
||||
{
|
||||
|
||||
@@ -56,6 +56,17 @@ namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
Username = @"flyte",
|
||||
Id = 3103765,
|
||||
Rank = new APIUser.GlobalRank
|
||||
{
|
||||
Rank = null,
|
||||
},
|
||||
Team = new APITeam
|
||||
{
|
||||
Id = 2,
|
||||
Name = "mom?",
|
||||
ShortName = "MOM",
|
||||
FlagUrl = "https://assets.ppy.sh/teams/flag/1/b46fb10dbfd8a35dc50e6c00296c0dc6172dffc3ed3d3a4b379277ba498399fe.png",
|
||||
},
|
||||
CountryCode = CountryCode.JP,
|
||||
CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg",
|
||||
WasRecentlyOnline = true
|
||||
@@ -64,6 +75,17 @@ namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
Username = @"peppy",
|
||||
Id = 2,
|
||||
Rank = new APIUser.GlobalRank
|
||||
{
|
||||
Rank = 9999999,
|
||||
},
|
||||
Team = new APITeam
|
||||
{
|
||||
Id = 2,
|
||||
Name = "mom?",
|
||||
ShortName = "MOM",
|
||||
FlagUrl = "https://assets.ppy.sh/teams/flag/1/b46fb10dbfd8a35dc50e6c00296c0dc6172dffc3ed3d3a4b379277ba498399fe.png",
|
||||
},
|
||||
CountryCode = CountryCode.AU,
|
||||
CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg",
|
||||
IsSupporter = true,
|
||||
@@ -72,7 +94,18 @@ namespace osu.Game.Tests.Visual.Online
|
||||
new OnlineUserListPanel(new APIUser
|
||||
{
|
||||
Username = @"flyte",
|
||||
Rank = new APIUser.GlobalRank
|
||||
{
|
||||
Rank = null,
|
||||
},
|
||||
Id = 3103765,
|
||||
Team = new APITeam
|
||||
{
|
||||
Id = 2,
|
||||
Name = "mom?",
|
||||
ShortName = "MOM",
|
||||
FlagUrl = "https://assets.ppy.sh/teams/flag/1/b46fb10dbfd8a35dc50e6c00296c0dc6172dffc3ed3d3a4b379277ba498399fe.png",
|
||||
},
|
||||
CountryCode = CountryCode.JP,
|
||||
CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg",
|
||||
WasRecentlyOnline = true
|
||||
@@ -81,6 +114,17 @@ namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
Username = @"peppy",
|
||||
Id = 2,
|
||||
Rank = new APIUser.GlobalRank
|
||||
{
|
||||
Rank = 9999999,
|
||||
},
|
||||
Team = new APITeam
|
||||
{
|
||||
Id = 2,
|
||||
Name = "mom?",
|
||||
ShortName = "MOM",
|
||||
FlagUrl = "https://assets.ppy.sh/teams/flag/1/b46fb10dbfd8a35dc50e6c00296c0dc6172dffc3ed3d3a4b379277ba498399fe.png",
|
||||
},
|
||||
CountryCode = CountryCode.AU,
|
||||
CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg",
|
||||
LastVisit = DateTimeOffset.Now
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Overlays.Chat;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
public partial class TestSceneReportPopover : OsuTestScene
|
||||
{
|
||||
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
|
||||
|
||||
private ReportPopoverContainer popover = null!;
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUp()
|
||||
{
|
||||
AddStep("create popover", () =>
|
||||
{
|
||||
Child = new PopoverContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = popover = new ReportPopoverContainer(),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSuccess()
|
||||
{
|
||||
ChatReportRequest pendingRequest = null!;
|
||||
|
||||
AddStep("setup request handling", () =>
|
||||
{
|
||||
dummyAPI.HandleRequest += request =>
|
||||
{
|
||||
if (request is ChatReportRequest chatReportRequest)
|
||||
{
|
||||
pendingRequest = chatReportRequest;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
});
|
||||
AddStep("show popover", () => popover.ShowPopover());
|
||||
AddStep("input reason", () => this.ChildrenOfType<OsuTextBox>().First().Text = "reason");
|
||||
AddStep("send report", () => this.ChildrenOfType<Button>().First().TriggerClick());
|
||||
AddUntilStep("wait for loading layer to hide", () => this.ChildrenOfType<LoadingLayer>().First().IsPresent, () => Is.True);
|
||||
AddWaitStep("wait some", 3);
|
||||
AddStep("complete request", () => pendingRequest.TriggerSuccess());
|
||||
AddUntilStep("wait for loading layer to hide", () => this.ChildrenOfType<LoadingLayer>().First().IsPresent, () => Is.False);
|
||||
AddAssert("ensure form is not present", () => this.ChildrenOfType<ReverseChildIDFillFlowContainer<Drawable>>().First().IsPresent, () => Is.False);
|
||||
AddAssert("ensure confirmation is present", () => this.ChildrenOfType<ReportPopover<ChatReportReason>.ReportConfirmation>().First().IsPresent, () => Is.True);
|
||||
AddUntilStep("wait for popover to hide", () => this.ChildrenOfType<ReportPopoverContainer.TestReportPopover>().First().IsPresent, () => Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFailure()
|
||||
{
|
||||
ChatReportRequest pendingRequest = null!;
|
||||
|
||||
AddStep("setup request handling", () =>
|
||||
{
|
||||
dummyAPI.HandleRequest += request =>
|
||||
{
|
||||
if (request is ChatReportRequest chatReportRequest)
|
||||
{
|
||||
pendingRequest = chatReportRequest;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
});
|
||||
AddStep("show popover", () => popover.ShowPopover());
|
||||
AddStep("input reason", () => this.ChildrenOfType<OsuTextBox>().First().Text = "reason");
|
||||
AddStep("send report", () => this.ChildrenOfType<Button>().First().TriggerClick());
|
||||
AddUntilStep("wait for loading layer to hide", () => this.ChildrenOfType<LoadingLayer>().First().IsPresent, () => Is.True);
|
||||
AddWaitStep("wait some", 3);
|
||||
AddStep("fail request", () => pendingRequest.TriggerFailure(new APIException("test error", new HttpRequestException("test error"))));
|
||||
AddUntilStep("wait for loading layer to hide", () => this.ChildrenOfType<LoadingLayer>().First().IsPresent, () => Is.False);
|
||||
AddAssert("ensure form is present", () => this.ChildrenOfType<ReverseChildIDFillFlowContainer<Drawable>>().First().IsPresent, () => Is.True);
|
||||
AddAssert("ensure error is present", () => this.ChildrenOfType<ErrorTextFlowContainer>().First().IsPresent, () => Is.True);
|
||||
AddAssert("ensure confirmation is not present", () => this.ChildrenOfType<ReportPopover<ChatReportReason>.ReportConfirmation>().First().IsPresent, () => Is.False);
|
||||
}
|
||||
|
||||
protected partial class ReportPopoverContainer : Drawable, IHasPopover
|
||||
{
|
||||
public Popover GetPopover() => new TestReportPopover("test");
|
||||
|
||||
public partial class TestReportPopover : ReportPopover<ChatReportReason>
|
||||
{
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
public TestReportPopover(string name)
|
||||
: base($"Report {name}?")
|
||||
{
|
||||
}
|
||||
|
||||
protected override APIRequest GetRequest(ChatReportReason reason, string comment) => new ChatReportRequest(1, reason, comment);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.RankedPlay;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osu.Game.Tests.Visual.Multiplayer;
|
||||
@@ -18,14 +19,26 @@ namespace osu.Game.Tests.Visual.RankedPlay
|
||||
public abstract partial class RankedPlayTestScene : MultiplayerTestScene
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns 5 sample <see cref="APIBeatmap"/>s.
|
||||
/// Returns 5 sample of the chosen ruleset <see cref="APIBeatmap"/>s.
|
||||
/// </summary>
|
||||
protected static APIBeatmap[] GetSampleBeatmaps()
|
||||
protected static APIBeatmap[] GetSampleBeatmaps(RulesetInfo ruleset)
|
||||
{
|
||||
using var resourceStream = TestResources.OpenResource("Requests/api-beatmaps-rankedplay.json");
|
||||
using var reader = new StreamReader(resourceStream);
|
||||
switch (ruleset.OnlineID)
|
||||
{
|
||||
case 3:
|
||||
{
|
||||
using var resourceStream = TestResources.OpenResource("Requests/api-beatmaps-rankedplay-mania4k.json");
|
||||
using var reader = new StreamReader(resourceStream);
|
||||
return JsonConvert.DeserializeObject<APIBeatmap[]>(reader.ReadToEnd())!;
|
||||
}
|
||||
|
||||
return JsonConvert.DeserializeObject<APIBeatmap[]>(reader.ReadToEnd())!;
|
||||
default:
|
||||
{
|
||||
using var resourceStream = TestResources.OpenResource("Requests/api-beatmaps-rankedplay.json");
|
||||
using var reader = new StreamReader(resourceStream);
|
||||
return JsonConvert.DeserializeObject<APIBeatmap[]>(reader.ReadToEnd())!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -33,7 +46,12 @@ namespace osu.Game.Tests.Visual.RankedPlay
|
||||
/// </summary>
|
||||
public class BeatmapRequestHandler
|
||||
{
|
||||
public readonly APIBeatmap[] Beatmaps = GetSampleBeatmaps();
|
||||
public APIBeatmap[] Beatmaps;
|
||||
|
||||
public BeatmapRequestHandler(RulesetInfo ruleset)
|
||||
{
|
||||
Beatmaps = GetSampleBeatmaps(ruleset);
|
||||
}
|
||||
|
||||
public bool HandleRequest(APIRequest request)
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user