mirror of
https://github.com/ppy/osu.git
synced 2026-05-26 07:29:53 +08:00
Compare commits
21 Commits
2026.518.0
...
2026.522.0
+1
-1
@@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2026.513.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2026.521.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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")
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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,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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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", () =>
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -60,7 +60,7 @@ namespace osu.Game.Tests.Visual.RankedPlay
|
||||
|
||||
AddStep("set discard phase", () => MultiplayerClient.RankedPlayChangeStage(RankedPlayStage.CardDiscard).WaitSafely());
|
||||
|
||||
AddWaitStep("wait", 3);
|
||||
AddUntilStep("wait until cards are present", () => this.ChildrenOfType<PlayerHandOfCards.PlayerHandCard>().Count() == 5);
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
|
||||
@@ -7,6 +7,7 @@ using System;
|
||||
using System.Threading;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Testing;
|
||||
@@ -16,10 +17,12 @@ using osu.Game.Overlays.Dialog;
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class TestSceneDialogOverlay : OsuTestScene
|
||||
public partial class TestSceneDialogOverlay : OsuTestScene, IOverlayManager
|
||||
{
|
||||
private DialogOverlay overlay;
|
||||
|
||||
private readonly Bindable<OverlayActivation> overlayActivationMode = new Bindable<OverlayActivation>(OverlayActivation.All);
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
@@ -99,7 +102,8 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
Icon = FontAwesome.Regular.TrashAlt,
|
||||
HeaderText = @"Confirm deletion ofConfirm deletion ofConfirm deletion ofConfirm deletion ofConfirm deletion ofConfirm deletion of",
|
||||
BodyText = @"Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver. ",
|
||||
BodyText =
|
||||
@"Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver. ",
|
||||
Buttons = new PopupDialogButton[]
|
||||
{
|
||||
new PopupDialogOkButton
|
||||
@@ -116,6 +120,36 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
}));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPushWhileOverlayActivationDisabled()
|
||||
{
|
||||
PopupDialog dialog = null;
|
||||
|
||||
AddStep("set activation mode disabled", () => overlayActivationMode.Value = OverlayActivation.Disabled);
|
||||
|
||||
AddStep("push dialog", () =>
|
||||
{
|
||||
overlay.Push(dialog = new TestPopupDialog
|
||||
{
|
||||
Buttons = new PopupDialogButton[]
|
||||
{
|
||||
new PopupDialogOkButton { Text = @"OK" },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
AddUntilStep("overlay not visible", () => overlay.State.Value, () => Is.EqualTo(Visibility.Hidden));
|
||||
|
||||
AddStep("set activation mode enabled", () => overlayActivationMode.Value = OverlayActivation.All);
|
||||
|
||||
AddUntilStep("overlay visible", () => overlay.State.Value, () => Is.EqualTo(Visibility.Visible));
|
||||
AddUntilStep("dialog displayed", () => dialog.State.Value, () => Is.EqualTo(Visibility.Visible));
|
||||
AddStep("set activation mode disabled", () => overlayActivationMode.Value = OverlayActivation.Disabled);
|
||||
|
||||
AddUntilStep("dialog hidden", () => dialog.State.Value, () => Is.EqualTo(Visibility.Hidden));
|
||||
AddAssert("dialog dismissed", () => overlay.CurrentDialog, () => Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPushBeforeLoad()
|
||||
{
|
||||
@@ -194,5 +228,17 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
private partial class TestPopupDialog : PopupDialog
|
||||
{
|
||||
}
|
||||
|
||||
public IBindable<OverlayActivation> OverlayActivationMode => overlayActivationMode;
|
||||
|
||||
public IDisposable RegisterBlockingOverlay(OverlayContainer overlayContainer) => throw new NotImplementedException();
|
||||
|
||||
public void ShowBlockingOverlay(OverlayContainer overlay)
|
||||
{
|
||||
}
|
||||
|
||||
public void HideBlockingOverlay(OverlayContainer overlay)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,6 +188,12 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
Caption = "File selector",
|
||||
PlaceholderText = "Select a file",
|
||||
},
|
||||
new FormFileSelector
|
||||
{
|
||||
Caption = "File selector with deselection",
|
||||
PlaceholderText = "Select a file",
|
||||
AllowClear = true,
|
||||
},
|
||||
new FormBeatmapFileSelector(true)
|
||||
{
|
||||
Caption = "File selector with intermediate choice dialog",
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Overlays;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
public partial class TestSceneMigrateAudioDialog : OsuManualInputManagerTestScene
|
||||
{
|
||||
private DialogOverlay overlay = null!;
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("create dialog overlay", () => Child = overlay = new DialogOverlay());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestWasUsing()
|
||||
{
|
||||
AddStep("create dialog", () =>
|
||||
{
|
||||
overlay.Push(new MigrateNewAudioDialog(true));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNotUsing()
|
||||
{
|
||||
AddStep("create dialog", () =>
|
||||
{
|
||||
overlay.Push(new MigrateNewAudioDialog(false));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,8 +65,6 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
public SortedList<BreakPeriod> Breaks { get; set; } = new SortedList<BreakPeriod>(Comparer<BreakPeriod>.Default);
|
||||
|
||||
public List<string> UnhandledEventLines { get; set; } = new List<string>();
|
||||
|
||||
[JsonIgnore]
|
||||
public double TotalBreakTime => Breaks.Sum(b => b.Duration);
|
||||
|
||||
|
||||
@@ -72,7 +72,6 @@ namespace osu.Game.Beatmaps
|
||||
beatmap.ControlPointInfo = original.ControlPointInfo;
|
||||
beatmap.HitObjects = convertHitObjects(original.HitObjects, original, cancellationToken).OrderBy(s => s.StartTime).ToList();
|
||||
beatmap.Breaks = original.Breaks;
|
||||
beatmap.UnhandledEventLines = original.UnhandledEventLines;
|
||||
beatmap.AudioLeadIn = original.AudioLeadIn;
|
||||
beatmap.StackLeniency = original.StackLeniency;
|
||||
beatmap.SpecialStyle = original.SpecialStyle;
|
||||
|
||||
@@ -27,6 +27,7 @@ using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Storyboards;
|
||||
using osu.Game.Utils;
|
||||
using Realms;
|
||||
|
||||
@@ -216,7 +217,7 @@ namespace osu.Game.Beatmaps
|
||||
targetBeatmapSet.Beatmaps.Add(newBeatmap.BeatmapInfo);
|
||||
newBeatmap.BeatmapInfo.BeatmapSet = targetBeatmapSet;
|
||||
|
||||
save(newBeatmap.BeatmapInfo, newBeatmap, beatmapSkin, transferCollections: false);
|
||||
save(newBeatmap.BeatmapInfo, newBeatmap, beatmapSkin, new Storyboard(), transferCollections: false);
|
||||
|
||||
workingBeatmapCache.Invalidate(targetBeatmapSet);
|
||||
return GetWorkingBeatmap(newBeatmap.BeatmapInfo);
|
||||
@@ -359,8 +360,9 @@ namespace osu.Game.Beatmaps
|
||||
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to save the content against. The file referenced by <see cref="BeatmapInfo.Path"/> will be replaced.</param>
|
||||
/// <param name="beatmapContent">The <see cref="IBeatmap"/> content to write.</param>
|
||||
/// <param name="beatmapSkin">The beatmap <see cref="ISkin"/> content to write, null if to be omitted.</param>
|
||||
public virtual void Save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent, ISkin? beatmapSkin = null) =>
|
||||
save(beatmapInfo, beatmapContent, beatmapSkin, transferCollections: true);
|
||||
/// <param name="storyboard">The storyboard content to write, null if to be omitted.</param>
|
||||
public virtual void Save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent, ISkin? beatmapSkin = null, Storyboard? storyboard = null) =>
|
||||
save(beatmapInfo, beatmapContent, beatmapSkin, storyboard, transferCollections: true);
|
||||
|
||||
public void DeleteAllVideos()
|
||||
{
|
||||
@@ -502,7 +504,7 @@ namespace osu.Game.Beatmaps
|
||||
setInfo.Status = BeatmapOnlineStatus.LocallyModified;
|
||||
}
|
||||
|
||||
private void save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent, ISkin? beatmapSkin, bool transferCollections)
|
||||
private void save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent, ISkin? beatmapSkin, Storyboard? storyboard, bool transferCollections)
|
||||
{
|
||||
var setInfo = beatmapInfo.BeatmapSet;
|
||||
Debug.Assert(setInfo != null);
|
||||
@@ -526,7 +528,7 @@ namespace osu.Game.Beatmaps
|
||||
{
|
||||
using var stream = new MemoryStream();
|
||||
using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
|
||||
new LegacyBeatmapEncoder(beatmapContent, beatmapSkin).Encode(sw);
|
||||
new LegacyBeatmapEncoder(beatmapContent, beatmapSkin, storyboard).Encode(sw);
|
||||
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
|
||||
@@ -244,10 +244,10 @@ namespace osu.Game.Beatmaps.Drawables.Cards
|
||||
});
|
||||
|
||||
if (BeatmapSet.HasVideo)
|
||||
leftIconArea.Add(new VideoIconPill { IconSize = new Vector2(16) });
|
||||
leftIconArea.Add(new VideoIconPill());
|
||||
|
||||
if (BeatmapSet.HasStoryboard)
|
||||
leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(16) });
|
||||
leftIconArea.Add(new StoryboardIconPill());
|
||||
|
||||
if (BeatmapSet.FeaturedInSpotlight)
|
||||
{
|
||||
|
||||
@@ -226,10 +226,10 @@ namespace osu.Game.Beatmaps.Drawables.Cards
|
||||
});
|
||||
|
||||
if (BeatmapSet.HasVideo)
|
||||
leftIconArea.Add(new VideoIconPill { IconSize = new Vector2(16) });
|
||||
leftIconArea.Add(new VideoIconPill());
|
||||
|
||||
if (BeatmapSet.HasStoryboard)
|
||||
leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(16) });
|
||||
leftIconArea.Add(new StoryboardIconPill());
|
||||
|
||||
if (BeatmapSet.FeaturedInSpotlight)
|
||||
{
|
||||
|
||||
@@ -19,11 +19,11 @@ namespace osu.Game.Beatmaps.Formats
|
||||
{
|
||||
var output = CreateTemplateObject();
|
||||
foreach (LineBufferedReader stream in otherStreams.Prepend(primaryStream))
|
||||
ParseStreamInto(stream, output);
|
||||
ParseStreamInto(stream, stream == primaryStream, output);
|
||||
return output;
|
||||
}
|
||||
|
||||
protected abstract void ParseStreamInto(LineBufferedReader stream, TOutput output);
|
||||
protected abstract void ParseStreamInto(LineBufferedReader stream, bool isPrimaryStream, TOutput output);
|
||||
}
|
||||
|
||||
public abstract class Decoder
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace osu.Game.Beatmaps.Formats
|
||||
AddDecoder<Beatmap>("{", _ => new JsonBeatmapDecoder());
|
||||
}
|
||||
|
||||
protected override void ParseStreamInto(LineBufferedReader stream, Beatmap output)
|
||||
protected override void ParseStreamInto(LineBufferedReader stream, bool isPrimaryStream, Beatmap output)
|
||||
{
|
||||
stream.ReadToEnd().DeserializeInto(output);
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ namespace osu.Game.Beatmaps.Formats
|
||||
return templateBeatmap;
|
||||
}
|
||||
|
||||
protected override void ParseStreamInto(LineBufferedReader stream, Beatmap beatmap)
|
||||
protected override void ParseStreamInto(LineBufferedReader stream, bool isPrimaryStream, Beatmap beatmap)
|
||||
{
|
||||
this.beatmap = beatmap;
|
||||
this.beatmap.BeatmapVersion = FormatVersion;
|
||||
@@ -89,7 +89,7 @@ namespace osu.Game.Beatmaps.Formats
|
||||
|
||||
ApplyLegacyDefaults(this.beatmap);
|
||||
|
||||
base.ParseStreamInto(stream, beatmap);
|
||||
base.ParseStreamInto(stream, isPrimaryStream, beatmap);
|
||||
|
||||
applyDifficultyRestrictions(beatmap.Difficulty, beatmap);
|
||||
|
||||
@@ -204,7 +204,7 @@ namespace osu.Game.Beatmaps.Formats
|
||||
beatmap.BeatmapInfo.Ruleset = RulesetStore?.GetRuleset(0) ?? beatmap.BeatmapInfo.Ruleset;
|
||||
}
|
||||
|
||||
protected override void ParseLine(Beatmap beatmap, Section section, string line)
|
||||
protected override void ParseLine(Beatmap beatmap, Section section, string line, bool isPrimaryStream)
|
||||
{
|
||||
switch (section)
|
||||
{
|
||||
@@ -237,7 +237,7 @@ namespace osu.Game.Beatmaps.Formats
|
||||
return;
|
||||
}
|
||||
|
||||
base.ParseLine(beatmap, section, line);
|
||||
base.ParseLine(beatmap, section, line, isPrimaryStream);
|
||||
}
|
||||
|
||||
private void handleGeneral(string line)
|
||||
@@ -430,10 +430,6 @@ namespace osu.Game.Beatmaps.Formats
|
||||
{
|
||||
string[] split = line.Split(',');
|
||||
|
||||
// Until we have full storyboard encoder coverage, let's track any lines which aren't handled
|
||||
// and store them to a temporary location such that they aren't lost on editor save / export.
|
||||
bool lineSupportedByEncoder = false;
|
||||
|
||||
if (Enum.TryParse(split[0], out LegacyEventType type))
|
||||
{
|
||||
switch (type)
|
||||
@@ -445,7 +441,6 @@ namespace osu.Game.Beatmaps.Formats
|
||||
if (string.IsNullOrEmpty(beatmap.BeatmapInfo.Metadata.BackgroundFile))
|
||||
{
|
||||
beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[3]);
|
||||
lineSupportedByEncoder = true;
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -459,14 +454,12 @@ namespace osu.Game.Beatmaps.Formats
|
||||
if (!SupportedExtensions.VIDEO_EXTENSIONS.Contains(Path.GetExtension(filename).ToLowerInvariant()))
|
||||
{
|
||||
beatmap.BeatmapInfo.Metadata.BackgroundFile = filename;
|
||||
lineSupportedByEncoder = true;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case LegacyEventType.Background:
|
||||
beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[2]);
|
||||
lineSupportedByEncoder = true;
|
||||
break;
|
||||
|
||||
case LegacyEventType.Break:
|
||||
@@ -474,13 +467,9 @@ namespace osu.Game.Beatmaps.Formats
|
||||
double end = Math.Max(start, getOffsetTime(Parsing.ParseDouble(split[2])));
|
||||
|
||||
beatmap.Breaks.Add(new BreakPeriod(start, end));
|
||||
lineSupportedByEncoder = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!lineSupportedByEncoder)
|
||||
beatmap.UnhandledEventLines.Add(line);
|
||||
}
|
||||
|
||||
private void handleTimingPoint(string line)
|
||||
|
||||
@@ -14,6 +14,7 @@ using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Legacy;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Storyboards;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@@ -24,8 +25,8 @@ namespace osu.Game.Beatmaps.Formats
|
||||
public const int FIRST_LAZER_VERSION = 128;
|
||||
|
||||
private readonly IBeatmap beatmap;
|
||||
|
||||
private readonly ISkin? skin;
|
||||
private readonly LegacyStoryboardEncoder? storyboardEncoder;
|
||||
|
||||
private readonly int onlineRulesetID;
|
||||
|
||||
@@ -34,11 +35,18 @@ namespace osu.Game.Beatmaps.Formats
|
||||
/// </summary>
|
||||
/// <param name="beatmap">The beatmap to encode.</param>
|
||||
/// <param name="skin">The beatmap's skin, used for encoding combo colours.</param>
|
||||
public LegacyBeatmapEncoder(IBeatmap beatmap, ISkin? skin)
|
||||
/// <param name="storyboard">
|
||||
/// The combined storyboard, loaded from both the <c>.osu</c> and the <c>.osz</c>.
|
||||
/// Only elements from the <c>.osu</c> (marked via <see cref="StoryboardElementSource.Beatmap"/>) will be encoded to the beatmap.
|
||||
/// </param>
|
||||
public LegacyBeatmapEncoder(IBeatmap beatmap, ISkin? skin, Storyboard? storyboard)
|
||||
{
|
||||
this.beatmap = beatmap;
|
||||
this.skin = skin;
|
||||
|
||||
if (storyboard != null)
|
||||
storyboardEncoder = new LegacyStoryboardEncoder(storyboard);
|
||||
|
||||
onlineRulesetID = beatmap.BeatmapInfo.Ruleset.OnlineID;
|
||||
|
||||
if (onlineRulesetID < 0 || onlineRulesetID > 3)
|
||||
@@ -101,7 +109,12 @@ namespace osu.Game.Beatmaps.Formats
|
||||
writer.WriteLine(FormattableString.Invariant($@"CountdownOffset: {beatmap.CountdownOffset}"));
|
||||
if (onlineRulesetID == 3)
|
||||
writer.WriteLine(FormattableString.Invariant($"SpecialStyle: {(beatmap.SpecialStyle ? '1' : '0')}"));
|
||||
writer.WriteLine(FormattableString.Invariant($"WidescreenStoryboard: {(beatmap.WidescreenStoryboard ? '1' : '0')}"));
|
||||
|
||||
if (storyboardEncoder != null)
|
||||
storyboardEncoder.EncodeGeneralToBeatmap(writer);
|
||||
else
|
||||
writer.WriteLine(FormattableString.Invariant($"WidescreenStoryboard: {(beatmap.WidescreenStoryboard ? '1' : '0')}"));
|
||||
|
||||
if (beatmap.SamplesMatchPlaybackRate)
|
||||
writer.WriteLine(@"SamplesMatchPlaybackRate: 1");
|
||||
}
|
||||
@@ -151,14 +164,19 @@ namespace osu.Game.Beatmaps.Formats
|
||||
{
|
||||
writer.WriteLine("[Events]");
|
||||
|
||||
if (!string.IsNullOrEmpty(beatmap.BeatmapInfo.Metadata.BackgroundFile))
|
||||
writer.WriteLine(FormattableString.Invariant($"{(int)LegacyEventType.Background},0,\"{beatmap.BeatmapInfo.Metadata.BackgroundFile}\",0,0"));
|
||||
if (storyboardEncoder != null)
|
||||
{
|
||||
storyboardEncoder.EncodeEventsToBeatmap(writer);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!string.IsNullOrEmpty(beatmap.BeatmapInfo.Metadata.BackgroundFile))
|
||||
writer.WriteLine(FormattableString.Invariant($"{(int)LegacyEventType.Background},0,\"{beatmap.BeatmapInfo.Metadata.BackgroundFile}\",0,0"));
|
||||
}
|
||||
|
||||
writer.WriteLine("// Break Periods");
|
||||
foreach (var b in beatmap.Breaks)
|
||||
writer.WriteLine(FormattableString.Invariant($"{(int)LegacyEventType.Break},{b.StartTime},{b.EndTime}"));
|
||||
|
||||
foreach (string l in beatmap.UnhandledEventLines)
|
||||
writer.WriteLine(l);
|
||||
}
|
||||
|
||||
private void handleControlPoints(TextWriter writer)
|
||||
|
||||
@@ -35,7 +35,7 @@ namespace osu.Game.Beatmaps.Formats
|
||||
FormatVersion = version;
|
||||
}
|
||||
|
||||
protected override void ParseStreamInto(LineBufferedReader stream, T output)
|
||||
protected override void ParseStreamInto(LineBufferedReader stream, bool isPrimaryStream, T output)
|
||||
{
|
||||
Section section = Section.General;
|
||||
|
||||
@@ -65,7 +65,7 @@ namespace osu.Game.Beatmaps.Formats
|
||||
|
||||
try
|
||||
{
|
||||
ParseLine(output, section, line);
|
||||
ParseLine(output, section, line, isPrimaryStream);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -84,7 +84,7 @@ namespace osu.Game.Beatmaps.Formats
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual void ParseLine(T output, Section section, string line)
|
||||
protected virtual void ParseLine(T output, Section section, string line, bool isPrimaryStream)
|
||||
{
|
||||
switch (section)
|
||||
{
|
||||
|
||||
@@ -49,13 +49,13 @@ namespace osu.Game.Beatmaps.Formats
|
||||
return sb;
|
||||
}
|
||||
|
||||
protected override void ParseStreamInto(LineBufferedReader stream, Storyboard storyboard)
|
||||
protected override void ParseStreamInto(LineBufferedReader stream, bool isPrimaryStream, Storyboard storyboard)
|
||||
{
|
||||
this.storyboard = storyboard;
|
||||
base.ParseStreamInto(stream, storyboard);
|
||||
base.ParseStreamInto(stream, isPrimaryStream, storyboard);
|
||||
}
|
||||
|
||||
protected override void ParseLine(Storyboard storyboard, Section section, string line)
|
||||
protected override void ParseLine(Storyboard storyboard, Section section, string line, bool isPrimaryStream)
|
||||
{
|
||||
switch (section)
|
||||
{
|
||||
@@ -64,7 +64,7 @@ namespace osu.Game.Beatmaps.Formats
|
||||
return;
|
||||
|
||||
case Section.Events:
|
||||
handleEvents(line);
|
||||
handleEvents(line, isPrimaryStream);
|
||||
return;
|
||||
|
||||
case Section.Variables:
|
||||
@@ -72,7 +72,7 @@ namespace osu.Game.Beatmaps.Formats
|
||||
return;
|
||||
}
|
||||
|
||||
base.ParseLine(storyboard, section, line);
|
||||
base.ParseLine(storyboard, section, line, isPrimaryStream);
|
||||
}
|
||||
|
||||
private void handleGeneral(Storyboard storyboard, string line)
|
||||
@@ -91,7 +91,7 @@ namespace osu.Game.Beatmaps.Formats
|
||||
}
|
||||
}
|
||||
|
||||
private void handleEvents(string line)
|
||||
private void handleEvents(string line, bool isPrimaryStream)
|
||||
{
|
||||
decodeVariables(ref line);
|
||||
|
||||
@@ -116,8 +116,24 @@ namespace osu.Game.Beatmaps.Formats
|
||||
if (!Enum.TryParse(split[0], out LegacyEventType type))
|
||||
throw new InvalidDataException($@"Unknown event type: {split[0]}");
|
||||
|
||||
var source = isPrimaryStream ? StoryboardElementSource.Beatmap : StoryboardElementSource.Shared;
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case LegacyEventType.Background:
|
||||
{
|
||||
// the actual filename is handled in `LegacyBeatmapDecoder`.
|
||||
// this only handles the background offset, because it does not logically belong in `Beatmap` or related classes.
|
||||
if (split.Length > 4)
|
||||
{
|
||||
float x = Parsing.ParseFloat(split[3]);
|
||||
float y = Parsing.ParseFloat(split[4]);
|
||||
storyboard.BackgroundOffset = new Vector2(x, y);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case LegacyEventType.Video:
|
||||
{
|
||||
int offset = Parsing.ParseInt(split[1]);
|
||||
@@ -131,7 +147,7 @@ namespace osu.Game.Beatmaps.Formats
|
||||
if (!SupportedExtensions.VIDEO_EXTENSIONS.Contains(Path.GetExtension(path).ToLowerInvariant()))
|
||||
break;
|
||||
|
||||
storyboard.GetLayer("Video").Add(storyboardSprite = new StoryboardVideo(path, offset));
|
||||
storyboard.GetLayer("Video").Add(storyboardSprite = new StoryboardVideo(source, path, offset));
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -142,7 +158,7 @@ namespace osu.Game.Beatmaps.Formats
|
||||
string path = CleanFilename(split[3]);
|
||||
float x = Parsing.ParseFloat(split[4], Parsing.MAX_COORDINATE_VALUE);
|
||||
float y = Parsing.ParseFloat(split[5], Parsing.MAX_COORDINATE_VALUE);
|
||||
storyboardSprite = new StoryboardSprite(path, origin, new Vector2(x, y));
|
||||
storyboardSprite = new StoryboardSprite(source, path, origin, new Vector2(x, y));
|
||||
storyboard.GetLayer(layer).Add(storyboardSprite);
|
||||
break;
|
||||
}
|
||||
@@ -162,7 +178,7 @@ namespace osu.Game.Beatmaps.Formats
|
||||
frameDelay = Math.Round(0.015 * frameDelay) * 1.186 * (1000 / 60f);
|
||||
|
||||
var loopType = split.Length > 8 ? parseAnimationLoopType(split[8]) : AnimationLoopType.LoopForever;
|
||||
storyboardSprite = new StoryboardAnimation(path, origin, new Vector2(x, y), frameCount, frameDelay, loopType);
|
||||
storyboardSprite = new StoryboardAnimation(source, path, origin, new Vector2(x, y), frameCount, frameDelay, loopType);
|
||||
storyboard.GetLayer(layer).Add(storyboardSprite);
|
||||
break;
|
||||
}
|
||||
@@ -173,7 +189,7 @@ namespace osu.Game.Beatmaps.Formats
|
||||
string layer = parseLayer(split[2]);
|
||||
string path = CleanFilename(split[3]);
|
||||
float volume = split.Length > 4 ? Parsing.ParseFloat(split[4]) : 100;
|
||||
storyboard.GetLayer(layer).Add(new StoryboardSampleInfo(path, time, (int)volume));
|
||||
storyboard.GetLayer(layer).Add(new StoryboardSampleInfo(source, path, time, (int)volume));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -192,7 +208,8 @@ namespace osu.Game.Beatmaps.Formats
|
||||
string triggerName = split[1];
|
||||
double startTime = split.Length > 2 ? Parsing.ParseDouble(split[2]) : double.MinValue;
|
||||
double endTime = split.Length > 3 ? Parsing.ParseDouble(split[3]) : double.MaxValue;
|
||||
int groupNumber = split.Length > 4 ? Parsing.ParseInt(split[4]) : 0;
|
||||
// negation as per https://github.com/peppy/osu-stable-reference/blob/c34a74fb61c17c5667486a12548485d1f03baa2e/osu!/GameplayElements/HitObjectManager_LoadSave.cs#L736
|
||||
int groupNumber = split.Length > 4 ? -Parsing.ParseInt(split[4]) : 0;
|
||||
currentCommandsGroup = storyboardSprite?.AddTriggerGroup(triggerName, startTime, endTime, groupNumber);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,349 @@
|
||||
// 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.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Beatmaps.Legacy;
|
||||
using osu.Game.Storyboards;
|
||||
using osu.Game.Storyboards.Commands;
|
||||
|
||||
namespace osu.Game.Beatmaps.Formats
|
||||
{
|
||||
public class LegacyStoryboardEncoder
|
||||
{
|
||||
private readonly Storyboard storyboard;
|
||||
|
||||
public LegacyStoryboardEncoder(Storyboard storyboard)
|
||||
{
|
||||
this.storyboard = storyboard;
|
||||
}
|
||||
|
||||
#region Storyboards embedded in beatmaps
|
||||
|
||||
public void EncodeGeneralToBeatmap(TextWriter writer)
|
||||
{
|
||||
writer.WriteLine(FormattableString.Invariant($@"UseSkinSprites: {(storyboard.UseSkinSprites ? '1' : '0')}"));
|
||||
writer.WriteLine(FormattableString.Invariant($@"WidescreenStoryboard: {(storyboard.Beatmap.WidescreenStoryboard ? '1' : '0')}"));
|
||||
}
|
||||
|
||||
public void EncodeEventsToBeatmap(TextWriter writer)
|
||||
=> encodeEvents(writer, StoryboardElementSource.Beatmap);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Standalone storyboards
|
||||
|
||||
public void EncodeStandaloneStoryboard(TextWriter writer)
|
||||
{
|
||||
writer.WriteLine(@"[Events]");
|
||||
encodeEvents(writer, StoryboardElementSource.Shared);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void encodeEvents(TextWriter writer, StoryboardElementSource target)
|
||||
{
|
||||
// https://github.com/peppy/osu-stable-reference/blob/c34a74fb61c17c5667486a12548485d1f03baa2e/osu!/GameModes/Edit/Modes/EditorModeDesign.cs#L189
|
||||
// https://github.com/peppy/osu-stable-reference/blob/c34a74fb61c17c5667486a12548485d1f03baa2e/osu!/GameplayElements/Events/EventManager.cs#L368
|
||||
writer.WriteLine(@"// Background and Video events");
|
||||
|
||||
if (target == StoryboardElementSource.Beatmap)
|
||||
{
|
||||
// https://github.com/peppy/osu-stable-reference/blob/c34a74fb61c17c5667486a12548485d1f03baa2e/osu!/GameplayElements/HitObjectManager_LoadSave.cs#L1499
|
||||
writer.WriteLine(string.Format(CultureInfo.InvariantCulture,
|
||||
@"{0},{1},""{2}"",{3},{4}",
|
||||
(int)LegacyEventType.Background, 0, storyboard.BeatmapInfo.Metadata.BackgroundFile, storyboard.BackgroundOffset.X, storyboard.BackgroundOffset.Y));
|
||||
}
|
||||
|
||||
// https://github.com/peppy/osu-stable-reference/blob/c34a74fb61c17c5667486a12548485d1f03baa2e/osu!/GameplayElements/HitObjectManager_LoadSave.cs#L1496
|
||||
foreach (var video in storyboard.GetLayer(@"Video").Elements.OfType<StoryboardVideo>().Where(v => v.Source == target))
|
||||
{
|
||||
writer.WriteLine(string.Format(CultureInfo.InvariantCulture,
|
||||
@"{0},{1},""{2}""",
|
||||
nameof(LegacyEventType.Video), video.StartTime, video.Path));
|
||||
encodeCommands(writer, video);
|
||||
}
|
||||
|
||||
foreach (var legacyLayer in Enum.GetValues<LegacyStoryLayer>().Except(LegacyStoryLayer.Video.Yield()))
|
||||
{
|
||||
writer.WriteLine(string.Format(CultureInfo.InvariantCulture,
|
||||
@"// Storyboard Layer {0} ({1})",
|
||||
(int)legacyLayer,
|
||||
legacyLayer));
|
||||
string layerName = legacyLayer.ToString();
|
||||
var layer = storyboard.GetLayer(layerName);
|
||||
encodeSpritesFromLayer(writer, layer, target);
|
||||
}
|
||||
|
||||
// https://github.com/peppy/osu-stable-reference/blob/c34a74fb61c17c5667486a12548485d1f03baa2e/osu!/GameplayElements/HitObjectManager_LoadSave.cs#L1478-L1481
|
||||
writer.WriteLine(@"// Storyboard Sound Samples");
|
||||
|
||||
foreach (var legacyLayer in Enum.GetValues<LegacyStoryLayer>().Except(LegacyStoryLayer.Video.Yield()))
|
||||
{
|
||||
string layerName = legacyLayer.ToString();
|
||||
var layer = storyboard.GetLayer(layerName);
|
||||
|
||||
foreach (var sample in layer.Elements.OfType<StoryboardSampleInfo>().Where(s => s.Source == target))
|
||||
{
|
||||
writer.WriteLine(string.Format(CultureInfo.InvariantCulture,
|
||||
@"{0},{1},{2},""{3}"",{4}",
|
||||
nameof(LegacyEventType.Sample),
|
||||
sample.StartTime,
|
||||
(int)legacyLayer,
|
||||
sample.Path,
|
||||
sample.Volume));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void encodeSpritesFromLayer(TextWriter writer, StoryboardLayer layer, StoryboardElementSource target)
|
||||
{
|
||||
foreach (var element in layer.Elements.Where(elem => elem.Source == target))
|
||||
{
|
||||
LegacyOrigins origin;
|
||||
|
||||
switch (element)
|
||||
{
|
||||
case StoryboardAnimation animation:
|
||||
{
|
||||
origin = convertOrigin(animation.Origin);
|
||||
// https://github.com/peppy/osu-stable-reference/blob/c34a74fb61c17c5667486a12548485d1f03baa2e/osu!/GameplayElements/HitObjectManager_LoadSave.cs#L1505-L1507
|
||||
writer.WriteLine(string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
@"{0},{1},{2},""{3}"",{4},{5},{6},{7},{8}",
|
||||
nameof(LegacyEventType.Animation),
|
||||
layer.Name,
|
||||
origin,
|
||||
animation.Path,
|
||||
animation.InitialPosition.X,
|
||||
animation.InitialPosition.Y,
|
||||
animation.FrameCount,
|
||||
animation.FrameDelay,
|
||||
animation.LoopType));
|
||||
|
||||
encodeCommands(writer, animation);
|
||||
break;
|
||||
}
|
||||
|
||||
case StoryboardSprite sprite:
|
||||
{
|
||||
origin = convertOrigin(sprite.Origin);
|
||||
// https://github.com/peppy/osu-stable-reference/blob/c34a74fb61c17c5667486a12548485d1f03baa2e/osu!/GameplayElements/HitObjectManager_LoadSave.cs#L1502
|
||||
writer.WriteLine(string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
@"{0},{1},{2},""{3}"",{4},{5}",
|
||||
nameof(LegacyEventType.Sprite),
|
||||
layer.Name,
|
||||
origin,
|
||||
sprite.Path,
|
||||
sprite.InitialPosition.X,
|
||||
sprite.InitialPosition.Y));
|
||||
|
||||
encodeCommands(writer, sprite);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void encodeCommands(TextWriter writer, StoryboardSprite sprite)
|
||||
{
|
||||
foreach (var loopingGroup in sprite.LoopingGroups)
|
||||
{
|
||||
writer.WriteLine(string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
@" L,{0},{1}",
|
||||
loopingGroup.StartTime, loopingGroup.TotalIterations));
|
||||
foreach (var command in loopingGroup.AllCommands)
|
||||
// see `StoryboardLoopingCommand` ctor for why `relativeToTime` is passed
|
||||
encodeCommand(writer, command, 2, relativeToTime: loopingGroup.StartTime);
|
||||
}
|
||||
|
||||
foreach (var command in sprite.Commands.AllCommands)
|
||||
encodeCommand(writer, command, 1);
|
||||
|
||||
foreach (var triggerGroup in sprite.TriggerGroups)
|
||||
{
|
||||
// https://github.com/peppy/osu-stable-reference/blob/c34a74fb61c17c5667486a12548485d1f03baa2e/osu!/GameplayElements/HitObjectManager_LoadSave.cs#L1564-L1572
|
||||
writer.Write(string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
@" T,{0}",
|
||||
triggerGroup.TriggerName));
|
||||
|
||||
if (triggerGroup.TriggerEndTime != 0)
|
||||
{
|
||||
writer.Write(string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
@",{0},{1}",
|
||||
triggerGroup.TriggerStartTime, triggerGroup.TriggerEndTime));
|
||||
}
|
||||
|
||||
if (triggerGroup.GroupNumber != 0)
|
||||
{
|
||||
writer.Write(string.Format(CultureInfo.InvariantCulture, @",{0}", -triggerGroup.GroupNumber));
|
||||
}
|
||||
|
||||
writer.WriteLine();
|
||||
|
||||
foreach (var command in triggerGroup.AllCommands)
|
||||
encodeCommand(writer, command, 2);
|
||||
}
|
||||
}
|
||||
|
||||
private void encodeCommand(TextWriter writer, IStoryboardCommand command, int depth, double relativeToTime = 0)
|
||||
{
|
||||
for (int i = 0; i < depth; ++i)
|
||||
writer.Write(' ');
|
||||
|
||||
string typeAcronym;
|
||||
string details;
|
||||
|
||||
if (command is IStoryboardLoopingCommand loopingCommand)
|
||||
command = loopingCommand.OriginalCommand;
|
||||
|
||||
// https://github.com/peppy/osu-stable-reference/blob/c34a74fb61c17c5667486a12548485d1f03baa2e/osu!/GameplayElements/HitObjectManager_LoadSave.cs#L1546-L1550
|
||||
// https://github.com/peppy/osu-stable-reference/blob/c34a74fb61c17c5667486a12548485d1f03baa2e/osu!/GameplayElements/HitObjectManager_LoadSave.cs#L1690-L1730
|
||||
switch (command)
|
||||
{
|
||||
case StoryboardVectorScaleCommand vectorScale:
|
||||
typeAcronym = @"V";
|
||||
details = vectorScale.StartValue == vectorScale.EndValue
|
||||
? string.Format(CultureInfo.InvariantCulture, @"{0},{1}", vectorScale.StartValue.X, vectorScale.StartValue.Y)
|
||||
: string.Format(CultureInfo.InvariantCulture,
|
||||
@"{0},{1},{2},{3}",
|
||||
vectorScale.StartValue.X, vectorScale.StartValue.Y, vectorScale.EndValue.X, vectorScale.EndValue.Y);
|
||||
break;
|
||||
|
||||
case StoryboardAlphaCommand fade:
|
||||
typeAcronym = @"F";
|
||||
details = fade.StartValue == fade.EndValue
|
||||
? fade.StartValue.ToString(CultureInfo.InvariantCulture)
|
||||
: string.Format(CultureInfo.InvariantCulture,
|
||||
@"{0},{1}",
|
||||
fade.StartValue,
|
||||
fade.EndValue);
|
||||
break;
|
||||
|
||||
case StoryboardRotationCommand rotation:
|
||||
typeAcronym = @"R";
|
||||
details = rotation.StartValue == rotation.EndValue
|
||||
? rotation.StartValue.ToString(CultureInfo.InvariantCulture)
|
||||
: string.Format(CultureInfo.InvariantCulture,
|
||||
@"{0},{1}",
|
||||
float.DegreesToRadians(rotation.StartValue),
|
||||
float.DegreesToRadians(rotation.EndValue));
|
||||
break;
|
||||
|
||||
case StoryboardScaleCommand scale:
|
||||
typeAcronym = @"S";
|
||||
details = scale.StartValue == scale.EndValue
|
||||
? scale.StartValue.ToString(CultureInfo.InvariantCulture)
|
||||
: string.Format(CultureInfo.InvariantCulture,
|
||||
@"{0},{1}",
|
||||
scale.StartValue,
|
||||
scale.EndValue);
|
||||
break;
|
||||
|
||||
// stable has M commands that combine X and Y movement, but we decompose those into X/Y with no way to undo
|
||||
|
||||
case StoryboardXCommand movementX:
|
||||
typeAcronym = @"MX";
|
||||
details = movementX.StartValue == movementX.EndValue
|
||||
? movementX.StartValue.ToString(CultureInfo.InvariantCulture)
|
||||
: string.Format(CultureInfo.InvariantCulture,
|
||||
@"{0},{1}",
|
||||
movementX.StartValue,
|
||||
movementX.EndValue);
|
||||
break;
|
||||
|
||||
case StoryboardYCommand movementY:
|
||||
typeAcronym = @"MY";
|
||||
details = movementY.StartValue == movementY.EndValue
|
||||
? movementY.StartValue.ToString(CultureInfo.InvariantCulture)
|
||||
: string.Format(CultureInfo.InvariantCulture,
|
||||
@"{0},{1}",
|
||||
movementY.StartValue,
|
||||
movementY.EndValue);
|
||||
break;
|
||||
|
||||
case StoryboardColourCommand colour:
|
||||
typeAcronym = @"C";
|
||||
details = colour.StartValue == colour.EndValue
|
||||
? string.Format(CultureInfo.InvariantCulture,
|
||||
@"{0},{1},{2}",
|
||||
(int)(colour.StartValue.R * 255), (int)(colour.StartValue.G * 255), (int)(colour.StartValue.B * 255))
|
||||
: string.Format(CultureInfo.InvariantCulture,
|
||||
@"{0},{1},{2},{3},{4},{5}",
|
||||
(int)(colour.StartValue.R * 255), (int)(colour.StartValue.G * 255), (int)(colour.StartValue.B * 255),
|
||||
(int)(colour.EndValue.R * 255), (int)(colour.EndValue.G * 255), (int)(colour.EndValue.B * 255));
|
||||
break;
|
||||
|
||||
case StoryboardFlipHCommand:
|
||||
typeAcronym = @"P";
|
||||
details = @"H";
|
||||
break;
|
||||
|
||||
case StoryboardFlipVCommand:
|
||||
typeAcronym = @"P";
|
||||
details = @"V";
|
||||
break;
|
||||
|
||||
case StoryboardBlendingParametersCommand:
|
||||
typeAcronym = @"P";
|
||||
details = @"A";
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
writer.WriteLine(string.Format(CultureInfo.InvariantCulture,
|
||||
@"{0},{1},{2},{3},{4}",
|
||||
typeAcronym,
|
||||
(int)command.Easing,
|
||||
command.StartTime - relativeToTime,
|
||||
command.StartTime == command.EndTime ? null : command.EndTime - relativeToTime,
|
||||
details));
|
||||
}
|
||||
|
||||
private LegacyOrigins convertOrigin(Anchor anchor)
|
||||
{
|
||||
switch (anchor)
|
||||
{
|
||||
case Anchor.TopLeft:
|
||||
return LegacyOrigins.TopLeft;
|
||||
|
||||
case Anchor.TopCentre:
|
||||
return LegacyOrigins.TopCentre;
|
||||
|
||||
case Anchor.TopRight:
|
||||
return LegacyOrigins.TopRight;
|
||||
|
||||
case Anchor.CentreLeft:
|
||||
return LegacyOrigins.CentreLeft;
|
||||
|
||||
case Anchor.Centre:
|
||||
return LegacyOrigins.Centre;
|
||||
|
||||
case Anchor.CentreRight:
|
||||
return LegacyOrigins.CentreRight;
|
||||
|
||||
case Anchor.BottomLeft:
|
||||
return LegacyOrigins.BottomLeft;
|
||||
|
||||
case Anchor.BottomCentre:
|
||||
return LegacyOrigins.BottomCentre;
|
||||
|
||||
case Anchor.BottomRight:
|
||||
return LegacyOrigins.BottomRight;
|
||||
|
||||
default:
|
||||
return LegacyOrigins.TopLeft;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using System;
|
||||
using System.Diagnostics;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Timing;
|
||||
@@ -49,6 +50,11 @@ namespace osu.Game.Beatmaps
|
||||
[Resolved]
|
||||
private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private AudioManager audioManager { get; set; } = null!;
|
||||
|
||||
private Bindable<bool> experimentalAudio = null!;
|
||||
|
||||
public bool IsRewinding { get; private set; }
|
||||
|
||||
public FramedBeatmapClock(bool applyOffsets, bool requireDecoupling, IClock? source = null)
|
||||
@@ -66,9 +72,7 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
if (applyOffsets)
|
||||
{
|
||||
// Audio timings in general with newer BASS versions don't match stable.
|
||||
// This only seems to be required on windows. We need to eventually figure out why, with a bit of luck.
|
||||
platformOffsetClock = new OffsetCorrectionClock(interpolatedTrack) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 };
|
||||
platformOffsetClock = new OffsetCorrectionClock(interpolatedTrack);
|
||||
|
||||
// User global offset (set in settings) should also be applied.
|
||||
userGlobalOffsetClock = new OffsetCorrectionClock(platformOffsetClock);
|
||||
@@ -94,6 +98,9 @@ namespace osu.Game.Beatmaps
|
||||
userAudioOffset = config.GetBindable<double>(OsuSetting.AudioOffset);
|
||||
userAudioOffset.BindValueChanged(offset => userGlobalOffsetClock.Offset = offset.NewValue, true);
|
||||
|
||||
experimentalAudio = audioManager.UseExperimentalWasapi.GetBoundCopy();
|
||||
experimentalAudio.BindValueChanged(_ => updatePlatformOffset());
|
||||
|
||||
// TODO: this doesn't update when using ChangeSource() to change beatmap.
|
||||
beatmapOffsetSubscription = realm.SubscribeToPropertyChanged(
|
||||
r => r.Find<BeatmapInfo>(beatmap.Value.BeatmapInfo.ID)?.UserSettings,
|
||||
@@ -105,6 +112,39 @@ namespace osu.Game.Beatmaps
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Audio timings in general with newer BASS versions don't match stable.
|
||||
/// This only seems to be required on windows. We need to eventually figure out why, with a bit of luck.
|
||||
/// </summary>
|
||||
public const double WINDOWS_BASE_AUDIO_OFFSET = 15;
|
||||
|
||||
/// <summary>
|
||||
/// An additional offset applied to account for experimental mode being much better.
|
||||
/// </summary>
|
||||
public const double WINDOWS_EXPERIMENTAL_AUDIO_OFFSET = -25;
|
||||
|
||||
private void updatePlatformOffset()
|
||||
{
|
||||
if (!applyOffsets)
|
||||
return;
|
||||
|
||||
Debug.Assert(platformOffsetClock != null);
|
||||
|
||||
switch (RuntimeInfo.OS)
|
||||
{
|
||||
case RuntimeInfo.Platform.Windows:
|
||||
platformOffsetClock.Offset = WINDOWS_BASE_AUDIO_OFFSET;
|
||||
|
||||
if (audioManager.UseExperimentalWasapi.Value)
|
||||
platformOffsetClock.Offset += WINDOWS_EXPERIMENTAL_AUDIO_OFFSET;
|
||||
return;
|
||||
|
||||
default:
|
||||
platformOffsetClock.Offset = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
@@ -44,12 +44,6 @@ namespace osu.Game.Beatmaps
|
||||
/// </summary>
|
||||
SortedList<BreakPeriod> Breaks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// All lines from the [Events] section which aren't handled in the encoding process yet.
|
||||
/// These lines should be written out to the beatmap file on save or export.
|
||||
/// </summary>
|
||||
List<string> UnhandledEventLines { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Total amount of break time in the beatmap.
|
||||
/// </summary>
|
||||
|
||||
@@ -9,8 +9,8 @@ namespace osu.Game.Configuration
|
||||
{
|
||||
protected override string Filename => base.Filename.Replace(".ini", ".dev.ini");
|
||||
|
||||
public DevelopmentOsuConfigManager(Storage storage, GameHost? host = null)
|
||||
: base(storage, host)
|
||||
public DevelopmentOsuConfigManager(Storage storage)
|
||||
: base(storage)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
using osu.Game.Overlays.Settings.Sections.Audio;
|
||||
|
||||
namespace osu.Game.Configuration
|
||||
{
|
||||
public partial class MigrateNewAudioDialog : PopupDialog
|
||||
{
|
||||
[Cached]
|
||||
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
|
||||
|
||||
public MigrateNewAudioDialog(bool wasAlreadyUsing)
|
||||
{
|
||||
Icon = FontAwesome.Regular.Bell;
|
||||
|
||||
if (wasAlreadyUsing)
|
||||
{
|
||||
HeaderText = @"New audio engine is now default!";
|
||||
BodyText =
|
||||
$"""
|
||||
We recently added a new "Experimental Audio" backend for Windows users to reduce hitsound latency. Due to overwhelmingly positive feedback, this is now the default mode.
|
||||
|
||||
As you were already using this engine, your audio offset has been adjusted to account for an internal offset change (no intervention required).
|
||||
|
||||
If you have any issues, you can switch back to the legacy engine from settings via the "{AudioSettingsStrings.LegacyAudioLabel}" checkbox.
|
||||
""";
|
||||
}
|
||||
else
|
||||
{
|
||||
HeaderText = @"New audio engine has been enabled";
|
||||
BodyText =
|
||||
$"""
|
||||
We recently added a new "Experimental Audio" backend for Windows users to reduce hitsound latency. Due to overwhelmingly positive feedback, this is now the default mode.
|
||||
|
||||
If you have any issues, you can switch back to the legacy engine below, or at any time in settings via the "{AudioSettingsStrings.LegacyAudioLabel}" checkbox.
|
||||
""";
|
||||
|
||||
MainContent.Add(new Container
|
||||
{
|
||||
Margin = new MarginPadding { Top = 20 },
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Width = 400,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new LegacyAudioCheckbox(),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Buttons = new PopupDialogButton[]
|
||||
{
|
||||
new PopupDialogOkButton
|
||||
{
|
||||
Text = BeatmapOverlayStrings.UserContentConfirmButtonText,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,12 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Configuration;
|
||||
using osu.Framework.Configuration.Tracking;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Input.Handlers.Mouse;
|
||||
using osu.Framework.Input.Handlers.Pen;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Beatmaps.Drawables.Cards;
|
||||
@@ -33,14 +30,9 @@ namespace osu.Game.Configuration
|
||||
{
|
||||
public class OsuConfigManager : IniConfigManager<OsuSetting>, IGameplaySettings
|
||||
{
|
||||
private readonly GameHost? host;
|
||||
|
||||
public OsuConfigManager(Storage storage, GameHost? host = null)
|
||||
public OsuConfigManager(Storage storage)
|
||||
: base(storage)
|
||||
{
|
||||
this.host = host;
|
||||
|
||||
Migrate();
|
||||
}
|
||||
|
||||
protected override void InitialiseDefaults()
|
||||
@@ -258,42 +250,6 @@ namespace osu.Game.Configuration
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Migrate()
|
||||
{
|
||||
// arrives as 2020.123.0-lazer
|
||||
string rawVersion = Get<string>(OsuSetting.Version);
|
||||
|
||||
if (rawVersion.Length < 6)
|
||||
return;
|
||||
|
||||
string[] pieces = rawVersion.Split('.');
|
||||
|
||||
// on a fresh install or when coming from a non-release build, execution will end here.
|
||||
// we don't want to run migrations in such cases.
|
||||
if (!int.TryParse(pieces[0], out int year)) return;
|
||||
if (!int.TryParse(pieces[1], out int monthDay)) return;
|
||||
|
||||
int combined = year * 10000 + monthDay;
|
||||
|
||||
if (combined < 20250214)
|
||||
{
|
||||
// UI scaling on mobile platforms has been internally adjusted such that 1x UI scale looks correctly zoomed in than before.
|
||||
if (RuntimeInfo.IsMobile)
|
||||
GetBindable<float>(OsuSetting.UIScale).SetDefault();
|
||||
}
|
||||
|
||||
if (combined < 20250428)
|
||||
{
|
||||
// Pen tablet sensitivity is now separated from cursor sensitivity.
|
||||
// Most users will want the default to be what they already had set on cursor sensitivity so let's transfer it.
|
||||
var mouseHandler = host?.AvailableInputHandlers.OfType<MouseHandler>().SingleOrDefault();
|
||||
var penHandler = host?.AvailableInputHandlers.OfType<PenHandler>().SingleOrDefault();
|
||||
|
||||
if (penHandler != null && mouseHandler != null && penHandler.Sensitivity.IsDefault)
|
||||
penHandler.Sensitivity.Value = mouseHandler.Sensitivity.Value;
|
||||
}
|
||||
}
|
||||
|
||||
public override TrackedSettings CreateTrackedSettings()
|
||||
{
|
||||
return new TrackedSettings
|
||||
|
||||
@@ -67,6 +67,14 @@ namespace osu.Game.Database
|
||||
Configuration = new LegacySkinDecoder().Decode(skinStreamReader)
|
||||
};
|
||||
|
||||
using var storyboardStream = base.GetFileContents(model, file);
|
||||
|
||||
if (storyboardStream == null)
|
||||
return null;
|
||||
|
||||
using var storyboardStreamReader = new LineBufferedReader(storyboardStream);
|
||||
var beatmapStoryboard = new LegacyStoryboardDecoder().Decode(storyboardStreamReader);
|
||||
|
||||
MutateBeatmap(model, playableBeatmap);
|
||||
|
||||
// Encode to legacy format
|
||||
@@ -78,7 +86,7 @@ namespace osu.Game.Database
|
||||
// If we don't do that, uploads to BSS may show changes where there are none.
|
||||
sw.NewLine = "\r\n";
|
||||
|
||||
new LegacyBeatmapEncoder(playableBeatmap, beatmapSkin).Encode(sw);
|
||||
new LegacyBeatmapEncoder(playableBeatmap, beatmapSkin, beatmapStoryboard).Encode(sw);
|
||||
}
|
||||
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
@@ -93,6 +93,14 @@ namespace osu.Game.Graphics
|
||||
public static IconUsage EditorHitCircle => get(OsuIconMapping.EditorHitCircle);
|
||||
public static IconUsage EditorSlider => get(OsuIconMapping.EditorSlider);
|
||||
public static IconUsage EditorSpinner => get(OsuIconMapping.EditorSpinner);
|
||||
public static IconUsage EditorHit => get(OsuIconMapping.EditorHit);
|
||||
public static IconUsage EditorDrumRoll => get(OsuIconMapping.EditorDrumRoll);
|
||||
public static IconUsage EditorSwell => get(OsuIconMapping.EditorSwell);
|
||||
public static IconUsage EditorFruit => get(OsuIconMapping.EditorFruit);
|
||||
public static IconUsage EditorJuiceStream => get(OsuIconMapping.EditorJuiceStream);
|
||||
public static IconUsage EditorNote => get(OsuIconMapping.EditorNote);
|
||||
public static IconUsage EditorHoldNote => get(OsuIconMapping.EditorHoldNote);
|
||||
public static IconUsage EditorBananaShower => get(OsuIconMapping.EditorBananaShower);
|
||||
public static IconUsage EditorGrid => get(OsuIconMapping.EditorGrid);
|
||||
public static IconUsage EditorAddControlPoint => get(OsuIconMapping.EditorAddControlPoint);
|
||||
public static IconUsage EditorConvertToStream => get(OsuIconMapping.EditorConvertToStream);
|
||||
@@ -409,6 +417,30 @@ namespace osu.Game.Graphics
|
||||
[Description(@"Editor/spinner")]
|
||||
EditorSpinner,
|
||||
|
||||
[Description(@"Editor/hit")]
|
||||
EditorHit,
|
||||
|
||||
[Description(@"Editor/drum-roll")]
|
||||
EditorDrumRoll,
|
||||
|
||||
[Description(@"Editor/swell")]
|
||||
EditorSwell,
|
||||
|
||||
[Description(@"Editor/fruit")]
|
||||
EditorFruit,
|
||||
|
||||
[Description(@"Editor/juice-stream")]
|
||||
EditorJuiceStream,
|
||||
|
||||
[Description(@"Editor/banana-shower")]
|
||||
EditorBananaShower,
|
||||
|
||||
[Description(@"Editor/note")]
|
||||
EditorNote,
|
||||
|
||||
[Description(@"Editor/hold-note")]
|
||||
EditorHoldNote,
|
||||
|
||||
[Description(@"Editor/grid")]
|
||||
EditorGrid,
|
||||
|
||||
|
||||
@@ -21,9 +21,11 @@ using osu.Framework.Localisation;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using CommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterfaceV2
|
||||
{
|
||||
@@ -67,6 +69,12 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
/// </summary>
|
||||
public LocalisableString PlaceholderText { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// If set to <see langword="true"/>, the selector will display a button,
|
||||
/// which when clicked, will change <see cref="Current"/>'s value to <see langword="null"/>.
|
||||
/// </summary>
|
||||
public bool AllowClear { get; init; }
|
||||
|
||||
public Container PreviewContainer { get; private set; } = null!;
|
||||
|
||||
private FormControlBackground background = null!;
|
||||
@@ -180,7 +188,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
|
||||
private void onFileSelected()
|
||||
{
|
||||
if (Current.Value != null)
|
||||
if (Current.Value != null || AllowClear)
|
||||
this.HidePopover();
|
||||
|
||||
initialChooserPath = Current.Value?.DirectoryName;
|
||||
@@ -238,12 +246,12 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
|
||||
Task ICanAcceptFiles.Import(ImportTask[] tasks, ImportParameters parameters) => throw new NotImplementedException();
|
||||
|
||||
protected virtual FileChooserPopover CreatePopover(string[] handledExtensions, Bindable<FileInfo?> current, string? chooserPath) =>
|
||||
new FileChooserPopover(handledExtensions, current, chooserPath);
|
||||
protected virtual FileChooserPopover CreatePopover(string[] handledExtensions, Bindable<FileInfo?> current, string? chooserPath, bool allowClear) =>
|
||||
new FileChooserPopover(handledExtensions, current, chooserPath, allowClear);
|
||||
|
||||
public Popover GetPopover()
|
||||
{
|
||||
var popover = CreatePopover(handledExtensions, Current, initialChooserPath);
|
||||
var popover = CreatePopover(handledExtensions, Current, initialChooserPath, AllowClear);
|
||||
popoverState.UnbindBindings();
|
||||
popoverState.BindTo(popover.State);
|
||||
return popover;
|
||||
@@ -258,7 +266,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
|
||||
protected OsuFileSelector FileSelector;
|
||||
|
||||
public FileChooserPopover(string[] handledExtensions, Bindable<FileInfo?> current, string? chooserPath)
|
||||
public FileChooserPopover(string[] handledExtensions, Bindable<FileInfo?> current, string? chooserPath, bool allowClear)
|
||||
: base(false)
|
||||
{
|
||||
Child = new Container
|
||||
@@ -267,9 +275,37 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
// simplest solution to avoid underlying text to bleed through the bottom border
|
||||
// https://github.com/ppy/osu/pull/30005#issuecomment-2378884430
|
||||
Padding = new MarginPadding { Bottom = 1 },
|
||||
Child = FileSelector = new OsuFileSelector(chooserPath, handledExtensions)
|
||||
Children = new[]
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Bottom = allowClear ? 50 : 0 },
|
||||
Child = FileSelector = new OsuFileSelector(chooserPath, handledExtensions)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
},
|
||||
allowClear
|
||||
? new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
Padding = new MarginPadding(5),
|
||||
Child = new DangerousRoundedButton
|
||||
{
|
||||
Text = CommonStrings.ButtonsClear,
|
||||
Action = () => OnFileSelected(null),
|
||||
Enabled = { Value = current.Value != null },
|
||||
Padding = new MarginPadding(5),
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
Width = 60,
|
||||
}
|
||||
}
|
||||
: Empty()
|
||||
},
|
||||
};
|
||||
|
||||
@@ -308,7 +344,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
};
|
||||
}
|
||||
|
||||
protected virtual void OnFileSelected(FileInfo file) => current.Value = file;
|
||||
protected virtual void OnFileSelected(FileInfo? file) => current.Value = file;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,6 +155,8 @@ namespace osu.Game.Input.Bindings
|
||||
new KeyBinding(new[] { InputKey.Alt, InputKey.Left }, GlobalAction.EditorSeekToPreviousBookmark),
|
||||
new KeyBinding(new[] { InputKey.Alt, InputKey.Right }, GlobalAction.EditorSeekToNextBookmark),
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.L }, GlobalAction.EditorDiscardUnsavedChanges),
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.U }, GlobalAction.EditorSubmitBeatmap),
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.O }, GlobalAction.EditorEditExternally),
|
||||
};
|
||||
|
||||
private static IEnumerable<KeyBinding> editorTestPlayKeyBindings => new[]
|
||||
@@ -528,6 +530,12 @@ namespace osu.Game.Input.Bindings
|
||||
|
||||
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.NextSkin))]
|
||||
NextSkin,
|
||||
|
||||
[LocalisableDescription(typeof(EditorStrings), nameof(EditorStrings.SubmitBeatmap))]
|
||||
EditorSubmitBeatmap,
|
||||
|
||||
[LocalisableDescription(typeof(EditorStrings), nameof(EditorStrings.EditExternally))]
|
||||
EditorEditExternally
|
||||
}
|
||||
|
||||
public enum GlobalActionCategory
|
||||
|
||||
@@ -100,19 +100,14 @@ namespace osu.Game.Localisation
|
||||
public static LocalisableString AdjustBeatmapOffsetAutomaticallyTooltip => new TranslatableString(getKey(@"adjust_beatmap_offset_automatically_tooltip"), @"If enabled, the offset suggested from last play on a beatmap is automatically applied.");
|
||||
|
||||
/// <summary>
|
||||
/// "Use experimental audio mode"
|
||||
/// "Use legacy audio mode"
|
||||
/// </summary>
|
||||
public static LocalisableString WasapiLabel => new TranslatableString(getKey(@"wasapi_label"), @"Use experimental audio mode");
|
||||
public static LocalisableString LegacyAudioLabel => new TranslatableString(getKey(@"legacy_audio_label"), @"Use legacy audio mode");
|
||||
|
||||
/// <summary>
|
||||
/// "This will attempt to initialise the audio engine in a lower latency mode."
|
||||
/// "Use this if you are experiencing audio issues. Note that audio latency will be higher when this is toggled on."
|
||||
/// </summary>
|
||||
public static LocalisableString WasapiTooltip => new TranslatableString(getKey(@"wasapi_tooltip"), @"This will attempt to initialise the audio engine in a lower latency mode.");
|
||||
|
||||
/// <summary>
|
||||
/// "Due to reduced latency, your audio offset will need to be adjusted when enabling this setting. Generally expect to subtract 20 - 60 ms from your known value."
|
||||
/// </summary>
|
||||
public static LocalisableString WasapiNotice => new TranslatableString(getKey(@"wasapi_notice"), @"Due to reduced latency, your audio offset will need to be adjusted when enabling this setting. Generally expect to subtract 20 - 60 ms from your known value.");
|
||||
public static LocalisableString LegacyAudioTooltip => new TranslatableString(getKey(@"legacy_audio_tooltip"), @"Use this if you are experiencing audio issues. Note that audio latency will be higher when this is toggled on.");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
|
||||
@@ -42,8 +42,7 @@ namespace osu.Game.Localisation
|
||||
/// <summary>
|
||||
/// "If enabled, an "Are you ready? 3, 2, 1, GO!" countdown will be inserted at the beginning of the beatmap, assuming there is enough time to do so."
|
||||
/// </summary>
|
||||
public static LocalisableString CountdownDescription => new TranslatableString(getKey(@"countdown_description"),
|
||||
@"If enabled, an ""Are you ready? 3, 2, 1, GO!"" countdown will be inserted at the beginning of the beatmap, assuming there is enough time to do so.");
|
||||
public static LocalisableString CountdownDescription => new TranslatableString(getKey(@"countdown_description"), @"If enabled, an ""Are you ready? 3, 2, 1, GO!"" countdown will be inserted at the beginning of the beatmap, assuming there is enough time to do so.");
|
||||
|
||||
/// <summary>
|
||||
/// "Countdown speed"
|
||||
@@ -53,8 +52,7 @@ namespace osu.Game.Localisation
|
||||
/// <summary>
|
||||
/// "If the countdown sounds off-time, use this to make it appear one or more beats early."
|
||||
/// </summary>
|
||||
public static LocalisableString CountdownOffsetDescription =>
|
||||
new TranslatableString(getKey(@"countdown_offset_description"), @"If the countdown sounds off-time, use this to make it appear one or more beats early.");
|
||||
public static LocalisableString CountdownOffsetDescription => new TranslatableString(getKey(@"countdown_offset_description"), @"If the countdown sounds off-time, use this to make it appear one or more beats early.");
|
||||
|
||||
/// <summary>
|
||||
/// "Countdown offset"
|
||||
@@ -69,8 +67,7 @@ namespace osu.Game.Localisation
|
||||
/// <summary>
|
||||
/// "Allows storyboards to use the full screen space, rather than be confined to a 4:3 area."
|
||||
/// </summary>
|
||||
public static LocalisableString WidescreenSupportDescription =>
|
||||
new TranslatableString(getKey(@"widescreen_support_description"), @"Allows storyboards to use the full screen space, rather than be confined to a 4:3 area.");
|
||||
public static LocalisableString WidescreenSupportDescription => new TranslatableString(getKey(@"widescreen_support_description"), @"Allows storyboards to use the full screen space, rather than be confined to a 4:3 area.");
|
||||
|
||||
/// <summary>
|
||||
/// "Epilepsy warning"
|
||||
@@ -80,8 +77,7 @@ namespace osu.Game.Localisation
|
||||
/// <summary>
|
||||
/// "Recommended if the storyboard or video contain scenes with rapidly flashing colours."
|
||||
/// </summary>
|
||||
public static LocalisableString EpilepsyWarningDescription =>
|
||||
new TranslatableString(getKey(@"epilepsy_warning_description"), @"Recommended if the storyboard or video contain scenes with rapidly flashing colours.");
|
||||
public static LocalisableString EpilepsyWarningDescription => new TranslatableString(getKey(@"epilepsy_warning_description"), @"Recommended if the storyboard or video contain scenes with rapidly flashing colours.");
|
||||
|
||||
/// <summary>
|
||||
/// "Letterbox during breaks"
|
||||
@@ -91,8 +87,7 @@ namespace osu.Game.Localisation
|
||||
/// <summary>
|
||||
/// "Adds horizontal letterboxing to give a cinematic look during breaks."
|
||||
/// </summary>
|
||||
public static LocalisableString LetterboxDuringBreaksDescription =>
|
||||
new TranslatableString(getKey(@"letterbox_during_breaks_description"), @"Adds horizontal letterboxing to give a cinematic look during breaks.");
|
||||
public static LocalisableString LetterboxDuringBreaksDescription => new TranslatableString(getKey(@"letterbox_during_breaks_description"), @"Adds horizontal letterboxing to give a cinematic look during breaks.");
|
||||
|
||||
/// <summary>
|
||||
/// "Samples match playback rate"
|
||||
@@ -102,8 +97,7 @@ namespace osu.Game.Localisation
|
||||
/// <summary>
|
||||
/// "When enabled, all samples will speed up or slow down when rate-changing mods are enabled."
|
||||
/// </summary>
|
||||
public static LocalisableString SamplesMatchPlaybackRateDescription => new TranslatableString(getKey(@"samples_match_playback_rate_description"),
|
||||
@"When enabled, all samples will speed up or slow down when rate-changing mods are enabled.");
|
||||
public static LocalisableString SamplesMatchPlaybackRateDescription => new TranslatableString(getKey(@"samples_match_playback_rate_description"), @"When enabled, all samples will speed up or slow down when rate-changing mods are enabled.");
|
||||
|
||||
/// <summary>
|
||||
/// "The size of all hit objects"
|
||||
@@ -123,8 +117,7 @@ namespace osu.Game.Localisation
|
||||
/// <summary>
|
||||
/// "The harshness of hit windows and difficulty of special objects (ie. spinners)"
|
||||
/// </summary>
|
||||
public static LocalisableString OverallDifficultyDescription =>
|
||||
new TranslatableString(getKey(@"overall_difficulty_description"), @"The harshness of hit windows and difficulty of special objects (ie. spinners)");
|
||||
public static LocalisableString OverallDifficultyDescription => new TranslatableString(getKey(@"overall_difficulty_description"), @"The harshness of hit windows and difficulty of special objects (ie. spinners)");
|
||||
|
||||
/// <summary>
|
||||
/// "Tick Rate"
|
||||
@@ -134,8 +127,7 @@ namespace osu.Game.Localisation
|
||||
/// <summary>
|
||||
/// "Determines how many "ticks" are generated within long hit objects. A tick rate of 1 will generate ticks on each beat, 2 would be twice per beat, etc."
|
||||
/// </summary>
|
||||
public static LocalisableString TickRateDescription => new TranslatableString(getKey(@"tick_rate_description"),
|
||||
@"Determines how many ""ticks"" are generated within long hit objects. A tick rate of 1 will generate ticks on each beat, 2 would be twice per beat, etc.");
|
||||
public static LocalisableString TickRateDescription => new TranslatableString(getKey(@"tick_rate_description"), @"Determines how many ""ticks"" are generated within long hit objects. A tick rate of 1 will generate ticks on each beat, 2 would be twice per beat, etc.");
|
||||
|
||||
/// <summary>
|
||||
/// "Base Velocity"
|
||||
@@ -145,8 +137,7 @@ namespace osu.Game.Localisation
|
||||
/// <summary>
|
||||
/// "The base velocity of the beatmap, affecting things like slider velocity and scroll speed in some rulesets."
|
||||
/// </summary>
|
||||
public static LocalisableString BaseVelocityDescription => new TranslatableString(getKey(@"base_velocity_description"),
|
||||
@"The base velocity of the beatmap, affecting things like slider velocity and scroll speed in some rulesets.");
|
||||
public static LocalisableString BaseVelocityDescription => new TranslatableString(getKey(@"base_velocity_description"), @"The base velocity of the beatmap, affecting things like slider velocity and scroll speed in some rulesets.");
|
||||
|
||||
/// <summary>
|
||||
/// "Metadata"
|
||||
@@ -188,6 +179,16 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString AudioTrack => new TranslatableString(getKey(@"audio_track"), @"Audio Track");
|
||||
|
||||
/// <summary>
|
||||
/// "Video"
|
||||
/// </summary>
|
||||
public static LocalisableString Video => new TranslatableString(getKey(@"video"), @"Video");
|
||||
|
||||
/// <summary>
|
||||
/// "The video will be used instead of the static background, if present. Beatmap downloads are offered both with and without video, so if adding a video, a matching background should also be provided."
|
||||
/// </summary>
|
||||
public static LocalisableString VideoHint => new TranslatableString(getKey(@"video_hint"), @"The video will be used instead of the static background, if present. Beatmap downloads are offered both with and without video, so if adding a video, a matching background should also be provided.");
|
||||
|
||||
/// <summary>
|
||||
/// "Custom sample sets"
|
||||
/// </summary>
|
||||
@@ -198,6 +199,11 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString ClickToSelectTrack => new TranslatableString(getKey(@"click_to_select_track"), @"Click to select a track");
|
||||
|
||||
/// <summary>
|
||||
/// "Click to select a video"
|
||||
/// </summary>
|
||||
public static LocalisableString ClickToSelectVideo => new TranslatableString(getKey(@"click_to_select_video"), @"Click to select a video");
|
||||
|
||||
/// <summary>
|
||||
/// "Click to select a background image"
|
||||
/// </summary>
|
||||
@@ -221,14 +227,12 @@ namespace osu.Game.Localisation
|
||||
/// <summary>
|
||||
/// "Sync metadata with all difficulties"
|
||||
/// </summary>
|
||||
public static LocalisableString SyncMetadataWithAllDifficulties =>
|
||||
new TranslatableString(getKey(@"sync_metadata_with_all_difficulties"), @"Sync metadata with all difficulties");
|
||||
public static LocalisableString SyncMetadataWithAllDifficulties => new TranslatableString(getKey(@"sync_metadata_with_all_difficulties"), @"Sync metadata with all difficulties");
|
||||
|
||||
/// <summary>
|
||||
/// "Copies artist, title, source, and tags to all difficulties."
|
||||
/// </summary>
|
||||
public static LocalisableString SyncMetadataWithAllDifficultiesTooltip => new TranslatableString(getKey(@"sync_metadata_with_all_difficulties_tooltip"),
|
||||
@"Copies artist, title, source, and tags to all difficulties.");
|
||||
public static LocalisableString SyncMetadataWithAllDifficultiesTooltip => new TranslatableString(getKey(@"sync_metadata_with_all_difficulties_tooltip"), @"Copies artist, title, source, and tags to all difficulties.");
|
||||
|
||||
/// <summary>
|
||||
/// "Ruleset ({0})"
|
||||
@@ -260,6 +264,11 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString DragToSetBackground => new TranslatableString(getKey(@"drag_to_set_background"), @"Drag image here to set beatmap background!");
|
||||
|
||||
/// <summary>
|
||||
/// "Drag video here to set beatmap video!"
|
||||
/// </summary>
|
||||
public static LocalisableString DragToSetVideo => new TranslatableString(getKey(@"drag_to_set_video"), @"Drag video here to set beatmap video!");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using MessagePack;
|
||||
|
||||
namespace osu.Game.Online.Multiplayer
|
||||
{
|
||||
/// <summary>
|
||||
/// User requests to change their slot in the room.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public class ChangeSlotRequest : MatchUserRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The zero-based ID of the desired slot.
|
||||
/// </summary>
|
||||
[Key(0)]
|
||||
public byte SlotID { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ namespace osu.Game.Online.Multiplayer
|
||||
[Union(0, typeof(TeamVersusRoomState))] // IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types.
|
||||
[Union(1, typeof(MatchmakingRoomState))]
|
||||
[Union(2, typeof(RankedPlayRoomState))]
|
||||
[Union(3, typeof(StandardMatchRoomState))]
|
||||
public abstract class MatchRoomState
|
||||
{
|
||||
}
|
||||
|
||||
@@ -7,22 +7,20 @@ using MessagePack;
|
||||
namespace osu.Game.Online.Multiplayer.MatchTypes.TeamVersus
|
||||
{
|
||||
[MessagePackObject]
|
||||
public class TeamVersusRoomState : MatchRoomState
|
||||
public class TeamVersusRoomState : StandardMatchRoomState
|
||||
{
|
||||
[Key(0)]
|
||||
public List<MultiplayerTeam> Teams { get; set; } = new List<MultiplayerTeam>();
|
||||
|
||||
[Key(1)]
|
||||
public bool Locked { get; set; }
|
||||
|
||||
public static TeamVersusRoomState CreateDefault() =>
|
||||
public static TeamVersusRoomState CreateDefault(byte? maxParticipants = null) =>
|
||||
new TeamVersusRoomState
|
||||
{
|
||||
Teams =
|
||||
{
|
||||
new MultiplayerTeam { ID = 0, Name = "Team Red" },
|
||||
new MultiplayerTeam { ID = 1, Name = "Team Blue" },
|
||||
}
|
||||
},
|
||||
Slots = maxParticipants == null ? null : new int?[maxParticipants.Value]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ namespace osu.Game.Online.Multiplayer
|
||||
[Union(4, typeof(RankedPlayCardHandReplayRequest))]
|
||||
[Union(5, typeof(SetLockStateRequest))]
|
||||
[Union(6, typeof(RollRequest))]
|
||||
[Union(7, typeof(ChangeSlotRequest))]
|
||||
public abstract class MatchUserRequest
|
||||
{
|
||||
}
|
||||
|
||||
@@ -403,8 +403,9 @@ namespace osu.Game.Online.Multiplayer
|
||||
/// <param name="queueMode">The new queue mode, if any.</param>
|
||||
/// <param name="autoStartDuration">The new auto-start countdown duration, if any.</param>
|
||||
/// <param name="autoSkip">The new auto-skip setting.</param>
|
||||
/// <param name="maxParticipants">The new participant count limit, if any.</param>
|
||||
public Task ChangeSettings(Optional<string> name = default, Optional<string> password = default, Optional<MatchType> matchType = default, Optional<QueueMode> queueMode = default,
|
||||
Optional<TimeSpan> autoStartDuration = default, Optional<bool> autoSkip = default)
|
||||
Optional<TimeSpan> autoStartDuration = default, Optional<bool> autoSkip = default, Optional<byte?> maxParticipants = default)
|
||||
{
|
||||
if (Room == null)
|
||||
throw new InvalidOperationException("Must be joined to a match to change settings.");
|
||||
@@ -416,7 +417,8 @@ namespace osu.Game.Online.Multiplayer
|
||||
MatchType = matchType.GetOr(Room.Settings.MatchType),
|
||||
QueueMode = queueMode.GetOr(Room.Settings.QueueMode),
|
||||
AutoStartDuration = autoStartDuration.GetOr(Room.Settings.AutoStartDuration),
|
||||
AutoSkip = autoSkip.GetOr(Room.Settings.AutoSkip)
|
||||
AutoSkip = autoSkip.GetOr(Room.Settings.AutoSkip),
|
||||
MaxParticipants = maxParticipants.GetOr(Room.Settings.MaxParticipants),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,9 @@ namespace osu.Game.Online.Multiplayer
|
||||
[Key(6)]
|
||||
public bool AutoSkip { get; set; }
|
||||
|
||||
[Key(7)]
|
||||
public byte? MaxParticipants { get; set; }
|
||||
|
||||
[IgnoreMember]
|
||||
public bool AutoStartEnabled => AutoStartDuration != TimeSpan.Zero;
|
||||
|
||||
@@ -47,6 +50,7 @@ namespace osu.Game.Online.Multiplayer
|
||||
QueueMode = room.QueueMode;
|
||||
AutoStartDuration = room.AutoStartDuration;
|
||||
AutoSkip = room.AutoSkip;
|
||||
MaxParticipants = room.MaxParticipants;
|
||||
}
|
||||
|
||||
public bool Equals(MultiplayerRoomSettings? other)
|
||||
@@ -60,7 +64,8 @@ namespace osu.Game.Online.Multiplayer
|
||||
&& MatchType == other.MatchType
|
||||
&& QueueMode == other.QueueMode
|
||||
&& AutoStartDuration == other.AutoStartDuration
|
||||
&& AutoSkip == other.AutoSkip;
|
||||
&& AutoSkip == other.AutoSkip
|
||||
&& MaxParticipants == other.MaxParticipants;
|
||||
}
|
||||
|
||||
public override string ToString() => $"Name:{Name}"
|
||||
@@ -69,6 +74,7 @@ namespace osu.Game.Online.Multiplayer
|
||||
+ $" Item:{PlaylistItemId}"
|
||||
+ $" Queue:{QueueMode}"
|
||||
+ $" Start:{AutoStartDuration}"
|
||||
+ $" AutoSkip:{AutoSkip}";
|
||||
+ $" AutoSkip:{AutoSkip}"
|
||||
+ $" MaxParticipants:{MaxParticipants?.ToString() ?? "no limit"}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,14 +10,13 @@ namespace osu.Game.Online.Multiplayer
|
||||
{
|
||||
/// <summary>
|
||||
/// <para>
|
||||
/// If <see langword="true"/>, <see cref="MultiplayerRoomUserRole.Player"/>s will not be able to change teams by themselves in the room,
|
||||
/// If <see langword="true"/>, <see cref="MultiplayerRoomUserRole.Player"/>s will not be able to change teams and slots by themselves in the room,
|
||||
/// only <see cref="MultiplayerRoomUserRole.Referee"/>s will be able to change teams for the <see cref="MultiplayerRoomUserRole.Player"/>s.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// If <see langword="false"/>, any user can change their team in the room.
|
||||
/// If <see langword="false"/>, any user can change their team and slot in the room.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
// TODO: mention slots as well when slots are reimplemented
|
||||
[Key(0)]
|
||||
public bool Locked { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
// 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 MessagePack;
|
||||
|
||||
namespace osu.Game.Online.Multiplayer
|
||||
{
|
||||
[MessagePackObject]
|
||||
public class StandardMatchRoomState : MatchRoomState
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the room is currently locked.
|
||||
/// When locked, changes to slots (and teams, in team versus) cannot be performed by anyone but room referees.
|
||||
/// </summary>
|
||||
[Key(1)]
|
||||
public bool Locked { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The state of slots in the room.
|
||||
/// Linked to <see cref="MultiplayerRoomSettings.MaxParticipants"/>.
|
||||
/// <list type="bullet">
|
||||
/// <item>When <see cref="MultiplayerRoomSettings.MaxParticipants"/> is <see langword="null"/>, this property is also <see langword="null"/>.</item>
|
||||
/// <item>
|
||||
/// When <see cref="MultiplayerRoomSettings.MaxParticipants"/> is not <see langword="null"/>, this property is an array of that length.
|
||||
/// The items of that array represent either an empty slot (represented by <see langword="null"/>),
|
||||
/// or an user occupying that slot (represented by the ID of the relevant user).
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
[Key(2)]
|
||||
public int?[]? Slots { get; set; }
|
||||
|
||||
public static StandardMatchRoomState Create(byte? maxParticipants = null) =>
|
||||
new StandardMatchRoomState
|
||||
{
|
||||
Slots = maxParticipants == null ? null : new int?[maxParticipants.Value]
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -119,7 +119,7 @@ namespace osu.Game.Online.Rooms
|
||||
/// <summary>
|
||||
/// The maximum number of users allowed in the room.
|
||||
/// </summary>
|
||||
public int? MaxParticipants
|
||||
public byte? MaxParticipants
|
||||
{
|
||||
get => maxParticipants;
|
||||
set => SetField(ref maxParticipants, value);
|
||||
@@ -297,8 +297,8 @@ namespace osu.Game.Online.Rooms
|
||||
[JsonProperty("ends_at")]
|
||||
private DateTimeOffset? endDate;
|
||||
|
||||
// Not yet serialised (not implemented).
|
||||
private int? maxParticipants;
|
||||
[JsonProperty("max_participants")]
|
||||
private byte? maxParticipants;
|
||||
|
||||
[JsonProperty("participant_count")]
|
||||
private int participantCount;
|
||||
@@ -365,6 +365,7 @@ namespace osu.Game.Online.Rooms
|
||||
QueueMode = room.Settings.QueueMode;
|
||||
AutoStartDuration = room.Settings.AutoStartDuration;
|
||||
AutoSkip = room.Settings.AutoSkip;
|
||||
MaxParticipants = room.Settings.MaxParticipants;
|
||||
Host = room.Host != null ? new APIUser { Id = room.Host.UserID } : null;
|
||||
Playlist = room.Playlist.Select(p => new PlaylistItem(p)).ToArray();
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ namespace osu.Game.Online
|
||||
internal static readonly IReadOnlyList<(Type derivedType, Type baseType)> BASE_TYPE_MAPPING = new[]
|
||||
{
|
||||
// multiplayer
|
||||
(typeof(ChangeSlotRequest), typeof(MatchUserRequest)),
|
||||
(typeof(ChangeTeamRequest), typeof(MatchUserRequest)),
|
||||
(typeof(StartMatchCountdownRequest), typeof(MatchUserRequest)),
|
||||
(typeof(StopCountdownRequest), typeof(MatchUserRequest)),
|
||||
@@ -32,6 +33,7 @@ namespace osu.Game.Online
|
||||
(typeof(CountdownStartedEvent), typeof(MatchServerEvent)),
|
||||
(typeof(CountdownStoppedEvent), typeof(MatchServerEvent)),
|
||||
(typeof(RollEvent), typeof(MatchServerEvent)),
|
||||
(typeof(StandardMatchRoomState), typeof(MatchRoomState)),
|
||||
(typeof(TeamVersusRoomState), typeof(MatchRoomState)),
|
||||
(typeof(TeamVersusUserState), typeof(MatchUserState)),
|
||||
(typeof(MatchStartCountdown), typeof(MultiplayerCountdown)),
|
||||
|
||||
+70
-1
@@ -26,6 +26,8 @@ using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Input.Handlers.Mouse;
|
||||
using osu.Framework.Input.Handlers.Pen;
|
||||
using osu.Framework.Input.Handlers.Tablet;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Logging;
|
||||
@@ -159,6 +161,8 @@ namespace osu.Game
|
||||
|
||||
private OnScreenDisplay onScreenDisplay;
|
||||
|
||||
private DialogOverlay dialogOverlay;
|
||||
|
||||
[Resolved]
|
||||
private FrameworkConfigManager frameworkConfig { get; set; }
|
||||
|
||||
@@ -1045,6 +1049,7 @@ namespace osu.Game
|
||||
{ FrameworkSetting.VolumeUniversal, 0.6 },
|
||||
{ FrameworkSetting.VolumeMusic, 0.6 },
|
||||
{ FrameworkSetting.VolumeEffect, 0.6 },
|
||||
{ FrameworkSetting.AudioUseExperimentalWasapi, true },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1234,7 +1239,7 @@ namespace osu.Game
|
||||
}, rightFloatingOverlayContent.Add, true);
|
||||
|
||||
loadComponentSingleFile(new AccountCreationOverlay(), topMostOverlayContent.Add, true);
|
||||
loadComponentSingleFile<IDialogOverlay>(new DialogOverlay(), topMostOverlayContent.Add, true);
|
||||
loadComponentSingleFile<IDialogOverlay>(dialogOverlay = new DialogOverlay(), topMostOverlayContent.Add, true);
|
||||
loadComponentSingleFile(new MedalOverlay(), topMostOverlayContent.Add);
|
||||
|
||||
loadComponentSingleFile(new BackgroundDataStoreProcessor(), Add);
|
||||
@@ -1293,6 +1298,70 @@ namespace osu.Game
|
||||
|
||||
// Importantly, this should be run after binding PostNotification to the import handlers so they can present the import after game startup.
|
||||
handleStartupImport();
|
||||
|
||||
applyConfigMigrations();
|
||||
|
||||
// finally, update the version stored to the configuration.
|
||||
// this MUST happen after `applyConfigMigrations()` call, as it relies on comparing the previous version.
|
||||
// debug / local compilations will reset to a non-release string.
|
||||
LocalConfig.SetValue(OsuSetting.Version, Version);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply any migrations to configuration.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For database migrations, see <see cref="RealmAccess.applyMigrationsForVersion"/>.
|
||||
/// </remarks>
|
||||
private void applyConfigMigrations()
|
||||
{
|
||||
// arrives as 2020.123.0-lazer
|
||||
string rawVersion = LocalConfig.Get<string>(OsuSetting.Version);
|
||||
|
||||
if (rawVersion.Length < 6)
|
||||
return;
|
||||
|
||||
string[] pieces = rawVersion.Split('.');
|
||||
|
||||
// on a fresh install or when coming from a non-release build, execution will end here.
|
||||
// we don't want to run migrations in such cases.
|
||||
if (!int.TryParse(pieces[0], out int year)) return;
|
||||
if (!int.TryParse(pieces[1], out int monthDay)) return;
|
||||
|
||||
int combined = year * 10000 + monthDay;
|
||||
|
||||
if (combined < 20250214)
|
||||
{
|
||||
// UI scaling on mobile platforms has been internally adjusted such that 1x UI scale looks correctly zoomed in than before.
|
||||
if (RuntimeInfo.IsMobile)
|
||||
LocalConfig.GetBindable<float>(OsuSetting.UIScale).SetDefault();
|
||||
}
|
||||
|
||||
if (combined < 20260520)
|
||||
{
|
||||
// Pen tablet sensitivity is now separated from cursor sensitivity.
|
||||
// Most users will want the default to be what they already had set on cursor sensitivity so let's transfer it.
|
||||
var mouseHandler = Host?.AvailableInputHandlers.OfType<MouseHandler>().SingleOrDefault();
|
||||
var penHandler = Host?.AvailableInputHandlers.OfType<PenHandler>().SingleOrDefault();
|
||||
|
||||
if (penHandler != null && mouseHandler != null && penHandler.Sensitivity.IsDefault)
|
||||
penHandler.Sensitivity.Value = mouseHandler.Sensitivity.Value;
|
||||
}
|
||||
|
||||
if (combined < 20260521 && RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
|
||||
{
|
||||
bool wasAlreadyUsing = Audio.UseExperimentalWasapi.Value;
|
||||
|
||||
// see application of FramedBeatmapClock.WINDOWS_EXPERIMENTAL_AUDIO_OFFSET in FramedBeatmapClock.
|
||||
// this basically undoes this new offset assuming that users which have been using this setting for a while
|
||||
// already have had things tuned.
|
||||
if (wasAlreadyUsing)
|
||||
LocalConfig.SetValue(OsuSetting.AudioOffset, LocalConfig.Get<double>(OsuSetting.AudioOffset) - FramedBeatmapClock.WINDOWS_EXPERIMENTAL_AUDIO_OFFSET);
|
||||
|
||||
Audio.UseExperimentalWasapi.Value = true;
|
||||
|
||||
dialogOverlay.Push(new MigrateNewAudioDialog(wasAlreadyUsing));
|
||||
}
|
||||
}
|
||||
|
||||
private void handleBackButton()
|
||||
|
||||
@@ -541,8 +541,8 @@ namespace osu.Game
|
||||
Storage ??= host.Storage;
|
||||
|
||||
LocalConfig ??= UseDevelopmentServer
|
||||
? new DevelopmentOsuConfigManager(Storage, host)
|
||||
: new OsuConfigManager(Storage, host);
|
||||
? new DevelopmentOsuConfigManager(Storage)
|
||||
: new OsuConfigManager(Storage);
|
||||
|
||||
host.ExceptionThrown += onExceptionThrown;
|
||||
}
|
||||
@@ -590,7 +590,7 @@ namespace osu.Game
|
||||
/// <param name="path">The path to migrate to.</param>
|
||||
/// <returns>Whether migration succeeded to completion. If <c>false</c>, some files were left behind.</returns>
|
||||
/// <exception cref="TimeoutException"></exception>
|
||||
public bool Migrate(string path)
|
||||
public bool MigrateUserData(string path)
|
||||
{
|
||||
Logger.Log($@"Migrating osu! data from ""{Storage.GetFullPath(string.Empty)}"" to ""{path}""...");
|
||||
|
||||
|
||||
@@ -178,6 +178,7 @@ namespace osu.Game.Overlays.Dashboard.CurrentlyOnline
|
||||
break;
|
||||
|
||||
case UserActivity.InSoloGame:
|
||||
case UserActivity.PlayingDailyChallenge:
|
||||
case UserActivity.InMultiplayerGame:
|
||||
case UserActivity.InPlaylistGame:
|
||||
userPanel.CanSpectate.Value = true;
|
||||
|
||||
@@ -14,7 +14,6 @@ using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Backgrounds;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osuTK;
|
||||
@@ -41,6 +40,8 @@ namespace osu.Game.Overlays.Dialog
|
||||
private readonly TextFlowContainer header;
|
||||
private readonly TextFlowContainer body;
|
||||
|
||||
public Container MainContent { get; private set; }
|
||||
|
||||
private bool actionInvoked;
|
||||
|
||||
public IconUsage Icon
|
||||
@@ -222,6 +223,13 @@ namespace osu.Game.Overlays.Dialog
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Padding = new MarginPadding { Horizontal = 15 },
|
||||
},
|
||||
MainContent = new Container
|
||||
{
|
||||
Origin = Anchor.TopCentre,
|
||||
Anchor = Anchor.TopCentre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
},
|
||||
buttonsContainer = new FillFlowContainer<PopupDialogButton>
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
@@ -243,7 +251,7 @@ namespace osu.Game.Overlays.Dialog
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(AudioManager audio, OsuColour colours)
|
||||
private void load(AudioManager audio)
|
||||
{
|
||||
flashSample = audio.Samples.Get(@"UI/default-select-disabled");
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Logging;
|
||||
|
||||
namespace osu.Game.Overlays
|
||||
{
|
||||
@@ -28,8 +29,14 @@ namespace osu.Game.Overlays
|
||||
|
||||
public PopupDialog CurrentDialog { get; private set; }
|
||||
|
||||
public override bool IsPresent => Scheduler.HasPendingTasks
|
||||
|| dialogContainer.Children.Count > 0;
|
||||
public override bool IsPresent => (Scheduler.HasPendingTasks || dialogContainer.Children.Count > 0)
|
||||
// The following line ensures that dialogs are not presented while the dialog overlay
|
||||
// cannot be displayed. This is due to the `Schedule` usage inside `Push()`.
|
||||
//
|
||||
// Without this, a dialog pushed during disabled overlay activation mode would be presented,
|
||||
// but immediately dismissed without ever being seen by the user (see
|
||||
// https://github.com/ppy/osu/blob/ce5e54c9d27b17d460d99e774de502f9480fb710/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs#L131-L136).
|
||||
&& OverlayActivationMode.Value == OverlayActivation.All;
|
||||
|
||||
[CanBeNull]
|
||||
private IDisposable duckOperation;
|
||||
@@ -77,6 +84,7 @@ namespace osu.Game.Overlays
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.Log($"{nameof(DialogOverlay)}: Showing dialog {dialog}");
|
||||
dialogContainer.Add(dialog);
|
||||
Show();
|
||||
|
||||
@@ -98,6 +106,7 @@ namespace osu.Game.Overlays
|
||||
// Handle the case where the dialog is the currently displayed dialog.
|
||||
// In this scenario, the overlay itself should also be hidden.
|
||||
Hide();
|
||||
Logger.Log($"{nameof(DialogOverlay)}: Dismissing dialog {dialog}");
|
||||
CurrentDialog = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,9 +24,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
|
||||
|
||||
private AudioDeviceDropdown dropdown = null!;
|
||||
|
||||
private FormCheckBox? wasapiExperimental;
|
||||
|
||||
private readonly Bindable<SettingsNote.Data?> wasapiExperimentalNote = new Bindable<SettingsNote.Data?>();
|
||||
private FormCheckBox? legacyAudio;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
@@ -44,18 +42,12 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
|
||||
|
||||
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
|
||||
{
|
||||
Add(new SettingsItemV2(wasapiExperimental = new FormCheckBox
|
||||
Add(new SettingsItemV2(legacyAudio = new LegacyAudioCheckbox())
|
||||
{
|
||||
Caption = AudioSettingsStrings.WasapiLabel,
|
||||
HintText = AudioSettingsStrings.WasapiTooltip,
|
||||
Current = audio.UseExperimentalWasapi,
|
||||
})
|
||||
{
|
||||
Keywords = new[] { "wasapi", "latency", "exclusive" },
|
||||
Note = { BindTarget = wasapiExperimentalNote },
|
||||
Keywords = new[] { "wasapi", "latency", "exclusive", "legacy", "experimental" },
|
||||
});
|
||||
|
||||
wasapiExperimental.Current.ValueChanged += _ => onDeviceChanged(string.Empty);
|
||||
legacyAudio.Current.ValueChanged += _ => onDeviceChanged(string.Empty);
|
||||
}
|
||||
|
||||
audio.OnNewDevice += onDeviceChanged;
|
||||
@@ -65,18 +57,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
|
||||
onDeviceChanged(string.Empty);
|
||||
}
|
||||
|
||||
private void onDeviceChanged(string _)
|
||||
{
|
||||
updateItems();
|
||||
|
||||
if (wasapiExperimental != null)
|
||||
{
|
||||
if (wasapiExperimental.Current.Value)
|
||||
wasapiExperimentalNote.Value = new SettingsNote.Data(AudioSettingsStrings.WasapiNotice, SettingsNote.Type.Warning);
|
||||
else
|
||||
wasapiExperimentalNote.Value = null;
|
||||
}
|
||||
}
|
||||
private void onDeviceChanged(string _) => Scheduler.AddOnce(updateItems);
|
||||
|
||||
private void updateItems()
|
||||
{
|
||||
@@ -117,4 +98,37 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
|
||||
=> string.IsNullOrEmpty(item) ? CommonStrings.Default : base.GenerateItemText(item);
|
||||
}
|
||||
}
|
||||
|
||||
public partial class LegacyAudioCheckbox : FormCheckBox
|
||||
{
|
||||
private Bindable<bool> configExperimentalAudio = null!;
|
||||
|
||||
public LegacyAudioCheckbox()
|
||||
{
|
||||
Caption = AudioSettingsStrings.LegacyAudioLabel;
|
||||
HintText = AudioSettingsStrings.LegacyAudioTooltip;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(AudioManager audio)
|
||||
{
|
||||
configExperimentalAudio = audio.UseExperimentalWasapi.GetBoundCopy();
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
// Manual two-way binding because we're inverting what the framework exposes.
|
||||
Current.ValueChanged += legacy =>
|
||||
{
|
||||
configExperimentalAudio.Value = !legacy.NewValue;
|
||||
};
|
||||
|
||||
configExperimentalAudio.BindValueChanged(experimental =>
|
||||
{
|
||||
Current.Value = !experimental.NewValue;
|
||||
}, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
||||
});
|
||||
}
|
||||
|
||||
protected virtual bool PerformMigration() => game?.Migrate(destination.FullName) != false;
|
||||
protected virtual bool PerformMigration() => game?.MigrateUserData(destination.FullName) != false;
|
||||
|
||||
public override void OnEntering(ScreenTransitionEvent e)
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user