mirror of
https://github.com/ppy/osu.git
synced 2024-12-14 05:32:54 +08:00
Merge branch 'master' into bubble_mod_implementation_clean
This commit is contained in:
commit
b74c1c8334
@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
if (withModifiedSkin)
|
||||
{
|
||||
AddStep("change component scale", () => Player.ChildrenOfType<LegacyScoreCounter>().First().Scale = new Vector2(2f));
|
||||
AddStep("update target", () => Player.ChildrenOfType<SkinnableTargetContainer>().ForEach(LegacySkin.UpdateDrawableTarget));
|
||||
AddStep("update target", () => Player.ChildrenOfType<SkinComponentsContainer>().ForEach(LegacySkin.UpdateDrawableTarget));
|
||||
AddStep("exit player", () => Player.Exit());
|
||||
CreateTest();
|
||||
}
|
||||
|
@ -60,26 +60,24 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
[Test]
|
||||
public void TestCatcherHyperStateReverted()
|
||||
{
|
||||
DrawableCatchHitObject drawableObject1 = null;
|
||||
DrawableCatchHitObject drawableObject2 = null;
|
||||
JudgementResult result1 = null;
|
||||
JudgementResult result2 = null;
|
||||
AddStep("catch hyper fruit", () =>
|
||||
{
|
||||
attemptCatch(new Fruit { HyperDashTarget = new Fruit { X = 100 } }, out drawableObject1, out result1);
|
||||
result1 = attemptCatch(new Fruit { HyperDashTarget = new Fruit { X = 100 } });
|
||||
});
|
||||
AddStep("catch normal fruit", () =>
|
||||
{
|
||||
attemptCatch(new Fruit(), out drawableObject2, out result2);
|
||||
result2 = attemptCatch(new Fruit());
|
||||
});
|
||||
AddStep("revert second result", () =>
|
||||
{
|
||||
catcher.OnRevertResult(drawableObject2, result2);
|
||||
catcher.OnRevertResult(result2);
|
||||
});
|
||||
checkHyperDash(true);
|
||||
AddStep("revert first result", () =>
|
||||
{
|
||||
catcher.OnRevertResult(drawableObject1, result1);
|
||||
catcher.OnRevertResult(result1);
|
||||
});
|
||||
checkHyperDash(false);
|
||||
}
|
||||
@ -87,16 +85,15 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
[Test]
|
||||
public void TestCatcherAnimationStateReverted()
|
||||
{
|
||||
DrawableCatchHitObject drawableObject = null;
|
||||
JudgementResult result = null;
|
||||
AddStep("catch kiai fruit", () =>
|
||||
{
|
||||
attemptCatch(new TestKiaiFruit(), out drawableObject, out result);
|
||||
result = attemptCatch(new TestKiaiFruit());
|
||||
});
|
||||
checkState(CatcherAnimationState.Kiai);
|
||||
AddStep("revert result", () =>
|
||||
{
|
||||
catcher.OnRevertResult(drawableObject, result);
|
||||
catcher.OnRevertResult(result);
|
||||
});
|
||||
checkState(CatcherAnimationState.Idle);
|
||||
}
|
||||
@ -268,23 +265,19 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
|
||||
private void checkHyperDash(bool state) => AddAssert($"catcher is {(state ? "" : "not ")}hyper dashing", () => catcher.HyperDashing == state);
|
||||
|
||||
private void attemptCatch(CatchHitObject hitObject)
|
||||
{
|
||||
attemptCatch(() => hitObject, 1);
|
||||
}
|
||||
|
||||
private void attemptCatch(Func<CatchHitObject> hitObject, int count)
|
||||
{
|
||||
for (int i = 0; i < count; i++)
|
||||
attemptCatch(hitObject(), out _, out _);
|
||||
attemptCatch(hitObject());
|
||||
}
|
||||
|
||||
private void attemptCatch(CatchHitObject hitObject, out DrawableCatchHitObject drawableObject, out JudgementResult result)
|
||||
private JudgementResult attemptCatch(CatchHitObject hitObject)
|
||||
{
|
||||
hitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
drawableObject = createDrawableObject(hitObject);
|
||||
result = createResult(hitObject);
|
||||
var drawableObject = createDrawableObject(hitObject);
|
||||
var result = createResult(hitObject);
|
||||
applyResult(drawableObject, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void applyResult(DrawableCatchHitObject drawableObject, JudgementResult result)
|
||||
|
@ -4,6 +4,7 @@
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@ -27,12 +28,12 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
|
||||
public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
|
||||
{
|
||||
if (lookup is GlobalSkinComponentLookup targetComponent)
|
||||
if (lookup is SkinComponentsContainerLookup containerLookup)
|
||||
{
|
||||
switch (targetComponent.Lookup)
|
||||
switch (containerLookup.Target)
|
||||
{
|
||||
case GlobalSkinComponentLookup.LookupType.MainHUDComponents:
|
||||
var components = base.GetDrawableComponent(lookup) as SkinnableTargetComponentsContainer;
|
||||
case SkinComponentsContainerLookup.TargetArea.MainHUDComponents:
|
||||
var components = base.GetDrawableComponent(lookup) as Container;
|
||||
|
||||
if (providesComboCounter && components != null)
|
||||
{
|
||||
|
@ -63,12 +63,12 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
updateCombo(result.ComboAtJudgement + 1, judgedObject.AccentColour.Value);
|
||||
}
|
||||
|
||||
public void OnRevertResult(DrawableCatchHitObject judgedObject, JudgementResult result)
|
||||
public void OnRevertResult(JudgementResult result)
|
||||
{
|
||||
if (!result.Type.AffectsCombo() || !result.HasResult)
|
||||
return;
|
||||
|
||||
updateCombo(result.ComboAtJudgement, judgedObject.AccentColour.Value);
|
||||
updateCombo(result.ComboAtJudgement, null);
|
||||
}
|
||||
|
||||
private void updateCombo(int newCombo, Color4? hitObjectColour)
|
||||
|
@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)
|
||||
=> CatcherArea.OnNewResult((DrawableCatchHitObject)judgedObject, result);
|
||||
|
||||
private void onRevertResult(DrawableHitObject judgedObject, JudgementResult result)
|
||||
=> CatcherArea.OnRevertResult((DrawableCatchHitObject)judgedObject, result);
|
||||
private void onRevertResult(JudgementResult result)
|
||||
=> CatcherArea.OnRevertResult(result);
|
||||
}
|
||||
}
|
||||
|
@ -254,7 +254,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
}
|
||||
}
|
||||
|
||||
public void OnRevertResult(DrawableCatchHitObject drawableObject, JudgementResult result)
|
||||
public void OnRevertResult(JudgementResult result)
|
||||
{
|
||||
var catchResult = (CatchJudgementResult)result;
|
||||
|
||||
@ -268,8 +268,8 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
SetHyperDashState();
|
||||
}
|
||||
|
||||
caughtObjectContainer.RemoveAll(d => d.HitObject == drawableObject.HitObject, false);
|
||||
droppedObjectTarget.RemoveAll(d => d.HitObject == drawableObject.HitObject, false);
|
||||
caughtObjectContainer.RemoveAll(d => d.HitObject == result.HitObject, false);
|
||||
droppedObjectTarget.RemoveAll(d => d.HitObject == result.HitObject, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -73,10 +73,10 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
comboDisplay.OnNewResult(hitObject, result);
|
||||
}
|
||||
|
||||
public void OnRevertResult(DrawableCatchHitObject hitObject, JudgementResult result)
|
||||
public void OnRevertResult(JudgementResult result)
|
||||
{
|
||||
comboDisplay.OnRevertResult(hitObject, result);
|
||||
Catcher.OnRevertResult(hitObject, result);
|
||||
comboDisplay.OnRevertResult(result);
|
||||
Catcher.OnRevertResult(result);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
|
@ -0,0 +1,67 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
{
|
||||
public partial class TestSceneObjectPlacement : EditorTestScene
|
||||
{
|
||||
protected override Ruleset CreateEditorRuleset() => new ManiaRuleset();
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; } = null!;
|
||||
|
||||
[Test]
|
||||
public void TestPlacementBeforeTrackStart()
|
||||
{
|
||||
AddStep("Seek to 0", () => EditorClock.Seek(0));
|
||||
AddStep("Select note", () => InputManager.Key(Key.Number2));
|
||||
AddStep("Hover negative span", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<Container>().First(x => x.Name == "Icons").Children[0]);
|
||||
});
|
||||
AddStep("Click", () => InputManager.Click(MouseButton.Left));
|
||||
AddAssert("No notes placed", () => EditorBeatmap.HitObjects.All(x => x.StartTime >= 0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSeekOnNotePlacement()
|
||||
{
|
||||
double? initialTime = null;
|
||||
|
||||
AddStep("store initial time", () => initialTime = EditorClock.CurrentTime);
|
||||
AddStep("change seek setting to true", () => config.SetValue(OsuSetting.EditorAutoSeekOnPlacement, true));
|
||||
placeObject();
|
||||
AddUntilStep("wait for seek to complete", () => !EditorClock.IsSeeking);
|
||||
AddAssert("seeked forward to object", () => EditorClock.CurrentTime, () => Is.GreaterThan(initialTime));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNoSeekOnNotePlacement()
|
||||
{
|
||||
double? initialTime = null;
|
||||
|
||||
AddStep("store initial time", () => initialTime = EditorClock.CurrentTime);
|
||||
AddStep("change seek setting to false", () => config.SetValue(OsuSetting.EditorAutoSeekOnPlacement, false));
|
||||
placeObject();
|
||||
AddAssert("not seeking", () => !EditorClock.IsSeeking);
|
||||
AddAssert("time is unchanged", () => EditorClock.CurrentTime, () => Is.EqualTo(initialTime));
|
||||
}
|
||||
|
||||
private void placeObject()
|
||||
{
|
||||
AddStep("select note placement tool", () => InputManager.Key(Key.Number2));
|
||||
AddStep("move mouse to centre of last column", () => InputManager.MoveMouseTo(this.ChildrenOfType<Column>().Last().ScreenSpaceDrawQuad.Centre));
|
||||
AddStep("place note", () => InputManager.Click(MouseButton.Left));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
{
|
||||
public partial class TestScenePlacementBeforeTrackStart : EditorTestScene
|
||||
{
|
||||
protected override Ruleset CreateEditorRuleset() => new ManiaRuleset();
|
||||
|
||||
[Test]
|
||||
public void TestPlacement()
|
||||
{
|
||||
AddStep("Seek to 0", () => EditorClock.Seek(0));
|
||||
AddStep("Select note", () => InputManager.Key(Key.Number2));
|
||||
AddStep("Hover negative span", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<Container>().First(x => x.Name == "Icons").Children[0]);
|
||||
});
|
||||
AddStep("Click", () => InputManager.Click(MouseButton.Left));
|
||||
AddAssert("No notes placed", () => EditorBeatmap.HitObjects.All(x => x.StartTime >= 0));
|
||||
}
|
||||
}
|
||||
}
|
@ -69,8 +69,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
/// </summary>
|
||||
private double? releaseTime;
|
||||
|
||||
public override double MaximumJudgementOffset => Tail.MaximumJudgementOffset;
|
||||
|
||||
public DrawableHoldNote()
|
||||
: this(null)
|
||||
{
|
||||
|
@ -15,13 +15,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
/// </summary>
|
||||
public partial class DrawableHoldNoteTail : DrawableNote
|
||||
{
|
||||
/// <summary>
|
||||
/// Lenience of release hit windows. This is to make cases where the hold note release
|
||||
/// is timed alongside presses of other hit objects less awkward.
|
||||
/// Todo: This shouldn't exist for non-LegacyBeatmapDecoder beatmaps
|
||||
/// </summary>
|
||||
private const double release_window_lenience = 1.5;
|
||||
|
||||
protected override ManiaSkinComponents Component => ManiaSkinComponents.HoldNoteTail;
|
||||
|
||||
protected DrawableHoldNote HoldNote => (DrawableHoldNote)ParentHitObject;
|
||||
@ -40,14 +33,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
|
||||
public void UpdateResult() => base.UpdateResult(true);
|
||||
|
||||
public override double MaximumJudgementOffset => base.MaximumJudgementOffset * release_window_lenience;
|
||||
|
||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||
{
|
||||
Debug.Assert(HitObject.HitWindows != null);
|
||||
|
||||
// Factor in the release lenience
|
||||
timeOffset /= release_window_lenience;
|
||||
timeOffset /= TailNote.RELEASE_WINDOW_LENIENCE;
|
||||
|
||||
if (!userTriggered)
|
||||
{
|
||||
|
@ -81,6 +81,8 @@ namespace osu.Game.Rulesets.Mania.Objects
|
||||
/// </summary>
|
||||
public TailNote Tail { get; private set; }
|
||||
|
||||
public override double MaximumJudgementOffset => Tail.MaximumJudgementOffset;
|
||||
|
||||
/// <summary>
|
||||
/// The time between ticks of this hold.
|
||||
/// </summary>
|
||||
|
@ -10,6 +10,15 @@ namespace osu.Game.Rulesets.Mania.Objects
|
||||
{
|
||||
public class TailNote : Note
|
||||
{
|
||||
/// <summary>
|
||||
/// Lenience of release hit windows. This is to make cases where the hold note release
|
||||
/// is timed alongside presses of other hit objects less awkward.
|
||||
/// Todo: This shouldn't exist for non-LegacyBeatmapDecoder beatmaps
|
||||
/// </summary>
|
||||
public const double RELEASE_WINDOW_LENIENCE = 1.5;
|
||||
|
||||
public override Judgement CreateJudgement() => new ManiaJudgement();
|
||||
|
||||
public override double MaximumJudgementOffset => base.MaximumJudgementOffset * RELEASE_WINDOW_LENIENCE;
|
||||
}
|
||||
}
|
||||
|
31
osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutopilot.cs
Normal file
31
osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutopilot.cs
Normal file
@ -0,0 +1,31 @@
|
||||
// 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 osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
{
|
||||
public partial class TestSceneOsuModAutopilot : OsuModTestScene
|
||||
{
|
||||
[Test]
|
||||
public void TestInstantResume()
|
||||
{
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new OsuModAutopilot(),
|
||||
PassCondition = () => true,
|
||||
Autoplay = false,
|
||||
});
|
||||
|
||||
AddUntilStep("wait for gameplay start", () => Player.LocalUserPlaying.Value);
|
||||
AddStep("press pause", () => InputManager.PressKey(Key.Escape));
|
||||
AddUntilStep("wait until paused", () => Player.GameplayClockContainer.IsPaused.Value);
|
||||
AddStep("release pause", () => InputManager.ReleaseKey(Key.Escape));
|
||||
AddStep("press resume", () => InputManager.PressKey(Key.Escape));
|
||||
AddUntilStep("wait for resume", () => !Player.IsResuming);
|
||||
AddAssert("resumed", () => !Player.GameplayClockContainer.IsPaused.Value);
|
||||
}
|
||||
}
|
||||
}
|
@ -60,6 +60,8 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
// Generate the replay frames the cursor should follow
|
||||
replayFrames = new OsuAutoGenerator(drawableRuleset.Beatmap, drawableRuleset.Mods).Generate().Frames.Cast<OsuReplayFrame>().ToList();
|
||||
|
||||
drawableRuleset.UseResumeOverlay = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,8 +25,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
Origin = Anchor.Centre;
|
||||
}
|
||||
|
||||
public override double MaximumJudgementOffset => DrawableSpinner.HitObject.Duration;
|
||||
|
||||
/// <summary>
|
||||
/// Apply a judgement result.
|
||||
/// </summary>
|
||||
|
@ -71,8 +71,8 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
double startTime = StartTime + (float)(i + 1) / totalSpins * Duration;
|
||||
|
||||
AddNested(i < SpinsRequired
|
||||
? new SpinnerTick { StartTime = startTime }
|
||||
: new SpinnerBonusTick { StartTime = startTime });
|
||||
? new SpinnerTick { StartTime = startTime, SpinnerDuration = Duration }
|
||||
: new SpinnerBonusTick { StartTime = startTime, SpinnerDuration = Duration });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -11,10 +11,17 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
{
|
||||
public class SpinnerTick : OsuHitObject
|
||||
{
|
||||
/// <summary>
|
||||
/// Duration of the <see cref="Spinner"/> containing this spinner tick.
|
||||
/// </summary>
|
||||
public double SpinnerDuration { get; set; }
|
||||
|
||||
public override Judgement CreateJudgement() => new OsuSpinnerTickJudgement();
|
||||
|
||||
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
||||
|
||||
public override double MaximumJudgementOffset => SpinnerDuration;
|
||||
|
||||
public class OsuSpinnerTickJudgement : OsuJudgement
|
||||
{
|
||||
public override HitResult MaxResult => HitResult.SmallBonus;
|
||||
|
@ -37,8 +37,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
|
||||
protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.DrumRollTick), _ => new TickPiece());
|
||||
|
||||
public override double MaximumJudgementOffset => HitObject.HitWindow;
|
||||
|
||||
protected override void OnApply()
|
||||
{
|
||||
base.OnApply();
|
||||
|
@ -31,6 +31,8 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
||||
|
||||
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
||||
|
||||
public override double MaximumJudgementOffset => HitWindow;
|
||||
|
||||
protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit { StartTime = startTime };
|
||||
|
||||
public class StrongNestedHit : StrongNestedHitObject
|
||||
|
@ -74,7 +74,7 @@ namespace osu.Game.Tests.Skins
|
||||
}
|
||||
}
|
||||
|
||||
var editableTypes = SkinnableInfo.GetAllAvailableDrawables().Where(t => (Activator.CreateInstance(t) as ISkinnableDrawable)?.IsEditable == true);
|
||||
var editableTypes = SerialisedDrawableInfo.GetAllAvailableDrawables().Where(t => (Activator.CreateInstance(t) as ISerialisableDrawable)?.IsEditable == true);
|
||||
|
||||
Assert.That(instantiatedTypes, Is.EquivalentTo(editableTypes));
|
||||
}
|
||||
@ -88,7 +88,7 @@ namespace osu.Game.Tests.Skins
|
||||
var skin = new TestSkin(new SkinInfo(), null, storage);
|
||||
|
||||
Assert.That(skin.DrawableComponentInfo, Has.Count.EqualTo(2));
|
||||
Assert.That(skin.DrawableComponentInfo[GlobalSkinComponentLookup.LookupType.MainHUDComponents], Has.Length.EqualTo(9));
|
||||
Assert.That(skin.DrawableComponentInfo[SkinComponentsContainerLookup.TargetArea.MainHUDComponents], Has.Length.EqualTo(9));
|
||||
}
|
||||
}
|
||||
|
||||
@ -101,10 +101,10 @@ namespace osu.Game.Tests.Skins
|
||||
var skin = new TestSkin(new SkinInfo(), null, storage);
|
||||
|
||||
Assert.That(skin.DrawableComponentInfo, Has.Count.EqualTo(2));
|
||||
Assert.That(skin.DrawableComponentInfo[GlobalSkinComponentLookup.LookupType.MainHUDComponents], Has.Length.EqualTo(6));
|
||||
Assert.That(skin.DrawableComponentInfo[GlobalSkinComponentLookup.LookupType.SongSelect], Has.Length.EqualTo(1));
|
||||
Assert.That(skin.DrawableComponentInfo[SkinComponentsContainerLookup.TargetArea.MainHUDComponents], Has.Length.EqualTo(6));
|
||||
Assert.That(skin.DrawableComponentInfo[SkinComponentsContainerLookup.TargetArea.SongSelect], Has.Length.EqualTo(1));
|
||||
|
||||
var skinnableInfo = skin.DrawableComponentInfo[GlobalSkinComponentLookup.LookupType.SongSelect].First();
|
||||
var skinnableInfo = skin.DrawableComponentInfo[SkinComponentsContainerLookup.TargetArea.SongSelect].First();
|
||||
|
||||
Assert.That(skinnableInfo.Type, Is.EqualTo(typeof(SkinnableSprite)));
|
||||
Assert.That(skinnableInfo.Settings.First().Key, Is.EqualTo("sprite_name"));
|
||||
@ -115,10 +115,10 @@ namespace osu.Game.Tests.Skins
|
||||
using (var storage = new ZipArchiveReader(stream))
|
||||
{
|
||||
var skin = new TestSkin(new SkinInfo(), null, storage);
|
||||
Assert.That(skin.DrawableComponentInfo[GlobalSkinComponentLookup.LookupType.MainHUDComponents], Has.Length.EqualTo(8));
|
||||
Assert.That(skin.DrawableComponentInfo[GlobalSkinComponentLookup.LookupType.MainHUDComponents].Select(i => i.Type), Contains.Item(typeof(UnstableRateCounter)));
|
||||
Assert.That(skin.DrawableComponentInfo[GlobalSkinComponentLookup.LookupType.MainHUDComponents].Select(i => i.Type), Contains.Item(typeof(ColourHitErrorMeter)));
|
||||
Assert.That(skin.DrawableComponentInfo[GlobalSkinComponentLookup.LookupType.MainHUDComponents].Select(i => i.Type), Contains.Item(typeof(LegacySongProgress)));
|
||||
Assert.That(skin.DrawableComponentInfo[SkinComponentsContainerLookup.TargetArea.MainHUDComponents], Has.Length.EqualTo(8));
|
||||
Assert.That(skin.DrawableComponentInfo[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].Select(i => i.Type), Contains.Item(typeof(UnstableRateCounter)));
|
||||
Assert.That(skin.DrawableComponentInfo[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].Select(i => i.Type), Contains.Item(typeof(ColourHitErrorMeter)));
|
||||
Assert.That(skin.DrawableComponentInfo[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].Select(i => i.Type), Contains.Item(typeof(LegacySongProgress)));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,26 +1,23 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Lists;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Skinning.Legacy;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Storyboards;
|
||||
|
||||
@ -28,10 +25,10 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
public partial class TestSceneBeatmapSkinFallbacks : OsuPlayerTestScene
|
||||
{
|
||||
private ISkin currentBeatmapSkin;
|
||||
private ISkin currentBeatmapSkin = null!;
|
||||
|
||||
[Resolved]
|
||||
private SkinManager skinManager { get; set; }
|
||||
private SkinManager skinManager { get; set; } = null!;
|
||||
|
||||
protected override bool HasCustomSteps => true;
|
||||
|
||||
@ -39,8 +36,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
public void TestEmptyLegacyBeatmapSkinFallsBack()
|
||||
{
|
||||
CreateSkinTest(TrianglesSkin.CreateInfo(), () => new LegacyBeatmapSkin(new BeatmapInfo(), null));
|
||||
AddUntilStep("wait for hud load", () => Player.ChildrenOfType<SkinnableTargetContainer>().All(c => c.ComponentsLoaded));
|
||||
AddAssert("hud from default skin", () => AssertComponentsFromExpectedSource(GlobalSkinComponentLookup.LookupType.MainHUDComponents, skinManager.CurrentSkin.Value));
|
||||
AddUntilStep("wait for hud load", () => Player.ChildrenOfType<SkinComponentsContainer>().All(c => c.ComponentsLoaded));
|
||||
AddAssert("hud from default skin", () => AssertComponentsFromExpectedSource(SkinComponentsContainerLookup.TargetArea.MainHUDComponents, skinManager.CurrentSkin.Value));
|
||||
}
|
||||
|
||||
protected void CreateSkinTest(SkinInfo gameCurrentSkin, Func<ISkin> getBeatmapSkin)
|
||||
@ -55,17 +52,17 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
});
|
||||
}
|
||||
|
||||
protected bool AssertComponentsFromExpectedSource(GlobalSkinComponentLookup.LookupType target, ISkin expectedSource)
|
||||
protected bool AssertComponentsFromExpectedSource(SkinComponentsContainerLookup.TargetArea target, ISkin expectedSource)
|
||||
{
|
||||
var actualComponentsContainer = Player.ChildrenOfType<SkinnableTargetContainer>().First(s => s.Target == target)
|
||||
.ChildrenOfType<SkinnableTargetComponentsContainer>().SingleOrDefault();
|
||||
var targetContainer = Player.ChildrenOfType<SkinComponentsContainer>().First(s => s.Lookup.Target == target);
|
||||
var actualComponentsContainer = targetContainer.ChildrenOfType<Container>().SingleOrDefault(c => c.Parent == targetContainer);
|
||||
|
||||
if (actualComponentsContainer == null)
|
||||
return false;
|
||||
|
||||
var actualInfo = actualComponentsContainer.CreateSkinnableInfo();
|
||||
var actualInfo = actualComponentsContainer.CreateSerialisedInfo();
|
||||
|
||||
var expectedComponentsContainer = (SkinnableTargetComponentsContainer)expectedSource.GetDrawableComponent(new GlobalSkinComponentLookup(target));
|
||||
var expectedComponentsContainer = expectedSource.GetDrawableComponent(new SkinComponentsContainerLookup(target)) as Container;
|
||||
if (expectedComponentsContainer == null)
|
||||
return false;
|
||||
|
||||
@ -86,23 +83,23 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
Add(expectedComponentsAdjustmentContainer);
|
||||
expectedComponentsAdjustmentContainer.UpdateSubTree();
|
||||
var expectedInfo = expectedComponentsContainer.CreateSkinnableInfo();
|
||||
var expectedInfo = expectedComponentsContainer.CreateSerialisedInfo();
|
||||
Remove(expectedComponentsAdjustmentContainer, true);
|
||||
|
||||
return almostEqual(actualInfo, expectedInfo);
|
||||
}
|
||||
|
||||
private static bool almostEqual(SkinnableInfo info, SkinnableInfo other) =>
|
||||
private static bool almostEqual(SerialisedDrawableInfo drawableInfo, SerialisedDrawableInfo? other) =>
|
||||
other != null
|
||||
&& info.Type == other.Type
|
||||
&& info.Anchor == other.Anchor
|
||||
&& info.Origin == other.Origin
|
||||
&& Precision.AlmostEquals(info.Position, other.Position, 1)
|
||||
&& Precision.AlmostEquals(info.Scale, other.Scale)
|
||||
&& Precision.AlmostEquals(info.Rotation, other.Rotation)
|
||||
&& info.Children.SequenceEqual(other.Children, new FuncEqualityComparer<SkinnableInfo>(almostEqual));
|
||||
&& drawableInfo.Type == other.Type
|
||||
&& drawableInfo.Anchor == other.Anchor
|
||||
&& drawableInfo.Origin == other.Origin
|
||||
&& Precision.AlmostEquals(drawableInfo.Position, other.Position, 1)
|
||||
&& Precision.AlmostEquals(drawableInfo.Scale, other.Scale)
|
||||
&& Precision.AlmostEquals(drawableInfo.Rotation, other.Rotation)
|
||||
&& drawableInfo.Children.SequenceEqual(other.Children, new FuncEqualityComparer<SerialisedDrawableInfo>(almostEqual));
|
||||
|
||||
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
|
||||
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null)
|
||||
=> new CustomSkinWorkingBeatmap(beatmap, storyboard, Clock, Audio, currentBeatmapSkin);
|
||||
|
||||
protected override Ruleset CreatePlayerRuleset() => new TestOsuRuleset();
|
||||
@ -111,7 +108,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
private readonly ISkin beatmapSkin;
|
||||
|
||||
public CustomSkinWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock referenceClock, AudioManager audio, ISkin beatmapSkin)
|
||||
public CustomSkinWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard, IFrameBasedClock referenceClock, AudioManager audio, ISkin beatmapSkin)
|
||||
: base(beatmap, storyboard, referenceClock, audio)
|
||||
{
|
||||
this.beatmapSkin = beatmapSkin;
|
||||
|
@ -1,10 +1,12 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
@ -14,16 +16,23 @@ using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Storyboards;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
public partial class TestSceneGameplaySampleTriggerSource : PlayerTestScene
|
||||
{
|
||||
private TestGameplaySampleTriggerSource sampleTriggerSource;
|
||||
private TestGameplaySampleTriggerSource sampleTriggerSource = null!;
|
||||
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
|
||||
|
||||
private Beatmap beatmap;
|
||||
private Beatmap beatmap = null!;
|
||||
|
||||
[Resolved]
|
||||
private AudioManager audio { get; set; } = null!;
|
||||
|
||||
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null)
|
||||
=> new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audio);
|
||||
|
||||
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
|
||||
{
|
||||
@ -39,12 +48,13 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
const double start_offset = 8000;
|
||||
const double spacing = 2000;
|
||||
|
||||
// intentionally start objects a bit late so we can test the case of no alive objects.
|
||||
double t = start_offset;
|
||||
|
||||
beatmap.HitObjects.AddRange(new[]
|
||||
{
|
||||
new HitCircle
|
||||
{
|
||||
// intentionally start objects a bit late so we can test the case of no alive objects.
|
||||
StartTime = t += spacing,
|
||||
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
|
||||
},
|
||||
@ -80,42 +90,66 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Test]
|
||||
public void TestCorrectHitObject()
|
||||
{
|
||||
HitObjectLifetimeEntry nextObjectEntry = null;
|
||||
waitForAliveObjectIndex(null);
|
||||
checkValidObjectIndex(0);
|
||||
|
||||
AddAssert("no alive objects", () => getNextAliveObject() == null);
|
||||
seekBeforeIndex(0);
|
||||
waitForAliveObjectIndex(0);
|
||||
checkValidObjectIndex(0);
|
||||
|
||||
AddAssert("check initially correct object", () => sampleTriggerSource.GetMostValidObject() == beatmap.HitObjects[0]);
|
||||
AddAssert("first object not hit", () => getNextAliveObject()?.Entry?.Result?.HasResult != true);
|
||||
|
||||
AddUntilStep("get next object", () =>
|
||||
AddStep("hit first object", () =>
|
||||
{
|
||||
var nextDrawableObject = getNextAliveObject();
|
||||
var next = getNextAliveObject();
|
||||
|
||||
if (nextDrawableObject != null)
|
||||
if (next != null)
|
||||
{
|
||||
nextObjectEntry = nextDrawableObject.Entry;
|
||||
InputManager.MoveMouseTo(nextDrawableObject.ScreenSpaceDrawQuad.Centre);
|
||||
return true;
|
||||
}
|
||||
Debug.Assert(next.Entry?.Result?.HasResult != true);
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
AddUntilStep("hit first hitobject", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(next.ScreenSpaceDrawQuad.Centre);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
return nextObjectEntry.Result?.HasResult == true;
|
||||
}
|
||||
});
|
||||
|
||||
AddAssert("check correct object after hit", () => sampleTriggerSource.GetMostValidObject() == beatmap.HitObjects[1]);
|
||||
AddAssert("first object hit", () => getNextAliveObject()?.Entry?.Result?.HasResult == true);
|
||||
|
||||
AddUntilStep("check correct object after miss", () => sampleTriggerSource.GetMostValidObject() == beatmap.HitObjects[2]);
|
||||
AddUntilStep("check correct object after miss", () => sampleTriggerSource.GetMostValidObject() == beatmap.HitObjects[3]);
|
||||
checkValidObjectIndex(1);
|
||||
|
||||
AddUntilStep("no alive objects", () => getNextAliveObject() == null);
|
||||
AddAssert("check correct object after none alive", () => sampleTriggerSource.GetMostValidObject() == beatmap.HitObjects[3]);
|
||||
// Still object 1 as it's not hit yet.
|
||||
seekBeforeIndex(1);
|
||||
waitForAliveObjectIndex(1);
|
||||
checkValidObjectIndex(1);
|
||||
|
||||
seekBeforeIndex(2);
|
||||
waitForAliveObjectIndex(2);
|
||||
checkValidObjectIndex(2);
|
||||
|
||||
seekBeforeIndex(3);
|
||||
waitForAliveObjectIndex(3);
|
||||
checkValidObjectIndex(3);
|
||||
|
||||
AddStep("Seek into future", () => Beatmap.Value.Track.Seek(beatmap.HitObjects.Last().GetEndTime() + 10000));
|
||||
|
||||
waitForAliveObjectIndex(null);
|
||||
checkValidObjectIndex(3);
|
||||
}
|
||||
|
||||
private DrawableHitObject getNextAliveObject() =>
|
||||
private void seekBeforeIndex(int index) =>
|
||||
AddStep($"seek to just before object {index}", () => Beatmap.Value.Track.Seek(beatmap.HitObjects[index].StartTime - 100));
|
||||
|
||||
private void waitForAliveObjectIndex(int? index)
|
||||
{
|
||||
if (index == null)
|
||||
AddUntilStep("wait for no alive objects", getNextAliveObject, () => Is.Null);
|
||||
else
|
||||
AddUntilStep($"wait for next alive to be {index}", () => getNextAliveObject()?.HitObject, () => Is.EqualTo(beatmap.HitObjects[index.Value]));
|
||||
}
|
||||
|
||||
private void checkValidObjectIndex(int index) =>
|
||||
AddAssert($"check valid object is {index}", () => sampleTriggerSource.GetMostValidObject(), () => Is.EqualTo(beatmap.HitObjects[index]));
|
||||
|
||||
private DrawableHitObject? getNextAliveObject() =>
|
||||
Player.DrawableRuleset.Playfield.HitObjectContainer.AliveObjects.FirstOrDefault();
|
||||
|
||||
[Test]
|
||||
|
@ -235,8 +235,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
createNew();
|
||||
|
||||
AddUntilStep("wait for hud load", () => hudOverlay.IsLoaded);
|
||||
AddUntilStep("wait for components to be hidden", () => hudOverlay.ChildrenOfType<SkinnableTargetContainer>().Single().Alpha == 0);
|
||||
AddUntilStep("wait for hud load", () => hudOverlay.ChildrenOfType<SkinnableTargetContainer>().All(c => c.ComponentsLoaded));
|
||||
AddUntilStep("wait for components to be hidden", () => hudOverlay.ChildrenOfType<SkinComponentsContainer>().Single().Alpha == 0);
|
||||
AddUntilStep("wait for hud load", () => hudOverlay.ChildrenOfType<SkinComponentsContainer>().All(c => c.ComponentsLoaded));
|
||||
|
||||
AddStep("bind on update", () =>
|
||||
{
|
||||
@ -254,10 +254,10 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
createNew();
|
||||
|
||||
AddUntilStep("wait for hud load", () => hudOverlay.IsLoaded);
|
||||
AddUntilStep("wait for components to be hidden", () => hudOverlay.ChildrenOfType<SkinnableTargetContainer>().Single().Alpha == 0);
|
||||
AddUntilStep("wait for components to be hidden", () => hudOverlay.ChildrenOfType<SkinComponentsContainer>().Single().Alpha == 0);
|
||||
|
||||
AddStep("reload components", () => hudOverlay.ChildrenOfType<SkinnableTargetContainer>().Single().Reload());
|
||||
AddUntilStep("skinnable components loaded", () => hudOverlay.ChildrenOfType<SkinnableTargetContainer>().Single().ComponentsLoaded);
|
||||
AddStep("reload components", () => hudOverlay.ChildrenOfType<SkinComponentsContainer>().Single().Reload());
|
||||
AddUntilStep("skinnable components loaded", () => hudOverlay.ChildrenOfType<SkinComponentsContainer>().Single().ComponentsLoaded);
|
||||
}
|
||||
|
||||
private void createNew(Action<HUDOverlay>? action = null)
|
||||
|
@ -19,7 +19,6 @@ using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
@ -37,6 +36,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
private TestDrawablePoolingRuleset drawableRuleset;
|
||||
|
||||
private TestPlayfield playfield => (TestPlayfield)drawableRuleset.Playfield;
|
||||
|
||||
[Test]
|
||||
public void TestReusedWithHitObjectsSpacedFarApart()
|
||||
{
|
||||
@ -133,29 +134,49 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddUntilStep("no DHOs shown", () => !this.ChildrenOfType<DrawableTestHitObject>().Any());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRevertResult()
|
||||
{
|
||||
ManualClock clock = null;
|
||||
Beatmap beatmap;
|
||||
|
||||
createTest(beatmap = new Beatmap
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new TestHitObject { StartTime = 0 },
|
||||
new TestHitObject { StartTime = 500 },
|
||||
new TestHitObject { StartTime = 1000 },
|
||||
}
|
||||
}, 10, () => new FramedClock(clock = new ManualClock()));
|
||||
|
||||
AddStep("fast forward to end", () => clock.CurrentTime = beatmap.HitObjects[^1].GetEndTime() + 100);
|
||||
AddUntilStep("all judged", () => playfield.JudgedObjects.Count, () => Is.EqualTo(3));
|
||||
|
||||
AddStep("rewind to middle", () => clock.CurrentTime = beatmap.HitObjects[1].StartTime - 100);
|
||||
AddUntilStep("some results reverted", () => playfield.JudgedObjects.Count, () => Is.EqualTo(1));
|
||||
|
||||
AddStep("fast forward to end", () => clock.CurrentTime = beatmap.HitObjects[^1].GetEndTime() + 100);
|
||||
AddUntilStep("all judged", () => playfield.JudgedObjects.Count, () => Is.EqualTo(3));
|
||||
|
||||
AddStep("disable frame stability", () => drawableRuleset.FrameStablePlayback = false);
|
||||
AddStep("instant seek to start", () => clock.CurrentTime = beatmap.HitObjects[0].StartTime - 100);
|
||||
AddAssert("all results reverted", () => playfield.JudgedObjects.Count, () => Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestApplyHitResultOnKilled()
|
||||
{
|
||||
ManualClock clock = null;
|
||||
bool anyJudged = false;
|
||||
|
||||
void onNewResult(JudgementResult _) => anyJudged = true;
|
||||
|
||||
var beatmap = new Beatmap();
|
||||
beatmap.HitObjects.Add(new TestKilledHitObject { Duration = 20 });
|
||||
|
||||
createTest(beatmap, 10, () => new FramedClock(clock = new ManualClock()));
|
||||
|
||||
AddStep("subscribe to new result", () =>
|
||||
{
|
||||
anyJudged = false;
|
||||
drawableRuleset.NewResult += onNewResult;
|
||||
});
|
||||
AddStep("skip past object", () => clock.CurrentTime = beatmap.HitObjects[0].GetEndTime() + 1000);
|
||||
|
||||
AddAssert("object judged", () => anyJudged);
|
||||
|
||||
AddStep("clean up", () => drawableRuleset.NewResult -= onNewResult);
|
||||
AddAssert("object judged", () => playfield.JudgedObjects.Count == 1);
|
||||
}
|
||||
|
||||
private void createTest(IBeatmap beatmap, int poolSize, Func<IFrameBasedClock> createClock = null)
|
||||
@ -212,12 +233,24 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
private partial class TestPlayfield : Playfield
|
||||
{
|
||||
public readonly HashSet<HitObject> JudgedObjects = new HashSet<HitObject>();
|
||||
|
||||
private readonly int poolSize;
|
||||
|
||||
public TestPlayfield(int poolSize)
|
||||
{
|
||||
this.poolSize = poolSize;
|
||||
AddInternal(HitObjectContainer);
|
||||
NewResult += (_, r) =>
|
||||
{
|
||||
Assert.That(JudgedObjects, Has.No.Member(r.HitObject));
|
||||
JudgedObjects.Add(r.HitObject);
|
||||
};
|
||||
RevertResult += r =>
|
||||
{
|
||||
Assert.That(JudgedObjects, Has.Member(r.HitObject));
|
||||
JudgedObjects.Remove(r.HitObject);
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
|
@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddUntilStep("wait for hud load", () => Player.ChildrenOfType<SkinnableTargetContainer>().All(c => c.ComponentsLoaded));
|
||||
AddUntilStep("wait for hud load", () => Player.ChildrenOfType<SkinComponentsContainer>().All(c => c.ComponentsLoaded));
|
||||
|
||||
AddStep("reload skin editor", () =>
|
||||
{
|
||||
|
@ -22,12 +22,18 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Cached(typeof(ScoreProcessor))]
|
||||
private TestScoreProcessor scoreProcessor = new TestScoreProcessor();
|
||||
|
||||
private readonly OsuHitWindows hitWindows = new OsuHitWindows();
|
||||
private readonly OsuHitWindows hitWindows;
|
||||
|
||||
private UnstableRateCounter counter;
|
||||
|
||||
private double prev;
|
||||
|
||||
public TestSceneUnstableRateCounter()
|
||||
{
|
||||
hitWindows = new OsuHitWindows();
|
||||
hitWindows.SetDifficulty(5);
|
||||
}
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUp()
|
||||
{
|
||||
|
@ -178,6 +178,7 @@ namespace osu.Game.Configuration
|
||||
SetDefault(OsuSetting.EditorDim, 0.25f, 0f, 0.75f, 0.25f);
|
||||
SetDefault(OsuSetting.EditorWaveformOpacity, 0.25f, 0f, 1f, 0.25f);
|
||||
SetDefault(OsuSetting.EditorShowHitMarkers, true);
|
||||
SetDefault(OsuSetting.EditorAutoSeekOnPlacement, true);
|
||||
|
||||
SetDefault(OsuSetting.LastProcessedMetadataId, -1);
|
||||
|
||||
@ -374,6 +375,7 @@ namespace osu.Game.Configuration
|
||||
SeasonalBackgroundMode,
|
||||
EditorWaveformOpacity,
|
||||
EditorShowHitMarkers,
|
||||
EditorAutoSeekOnPlacement,
|
||||
DiscordRichPresence,
|
||||
AutomaticallyDownloadWhenSpectating,
|
||||
ShowOnlineExplicitContent,
|
||||
|
@ -5,6 +5,7 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Utils;
|
||||
@ -43,7 +44,10 @@ namespace osu.Game.Database
|
||||
{
|
||||
string itemFilename = GetFilename(item).GetValidFilename();
|
||||
|
||||
IEnumerable<string> existingExports = exportStorage.GetFiles("", $"{itemFilename}*{FileExtension}");
|
||||
IEnumerable<string> existingExports =
|
||||
exportStorage
|
||||
.GetFiles(string.Empty, $"{itemFilename}*{FileExtension}")
|
||||
.Concat(exportStorage.GetDirectories(string.Empty));
|
||||
|
||||
string filename = NamingUtils.GetNextBestFilename(existingExports, $"{itemFilename}{FileExtension}");
|
||||
using (var stream = exportStorage.CreateFileSafely(filename))
|
||||
|
@ -1,12 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Extensions
|
||||
@ -48,42 +43,5 @@ namespace osu.Game.Extensions
|
||||
/// <returns>The delta vector in Parent's coordinates.</returns>
|
||||
public static Vector2 ScreenSpaceDeltaToParentSpace(this Drawable drawable, Vector2 delta) =>
|
||||
drawable.Parent.ToLocalSpace(drawable.Parent.ToScreenSpace(Vector2.Zero) + delta);
|
||||
|
||||
public static SkinnableInfo CreateSkinnableInfo(this Drawable component) => new SkinnableInfo(component);
|
||||
|
||||
public static void ApplySkinnableInfo(this Drawable component, SkinnableInfo info)
|
||||
{
|
||||
// todo: can probably make this better via deserialisation directly using a common interface.
|
||||
component.Position = info.Position;
|
||||
component.Rotation = info.Rotation;
|
||||
component.Scale = info.Scale;
|
||||
component.Anchor = info.Anchor;
|
||||
component.Origin = info.Origin;
|
||||
|
||||
if (component is ISkinnableDrawable skinnable)
|
||||
{
|
||||
skinnable.UsesFixedAnchor = info.UsesFixedAnchor;
|
||||
|
||||
foreach (var (_, property) in component.GetSettingsSourceProperties())
|
||||
{
|
||||
var bindable = ((IBindable)property.GetValue(component)!);
|
||||
|
||||
if (!info.Settings.TryGetValue(property.Name.ToSnakeCase(), out object? settingValue))
|
||||
{
|
||||
// TODO: We probably want to restore default if not included in serialisation information.
|
||||
// This is not simple to do as SetDefault() is only found in the typed Bindable<T> interface right now.
|
||||
continue;
|
||||
}
|
||||
|
||||
skinnable.CopyAdjustedSetting(bindable, settingValue);
|
||||
}
|
||||
}
|
||||
|
||||
if (component is Container container)
|
||||
{
|
||||
foreach (var child in info.Children)
|
||||
container.Add(child.CreateInstance());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ namespace osu.Game.Graphics.Containers
|
||||
/// </summary>
|
||||
public const float BREAK_LIGHTEN_AMOUNT = 0.3f;
|
||||
|
||||
protected const double BACKGROUND_FADE_DURATION = 800;
|
||||
public const double BACKGROUND_FADE_DURATION = 800;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not user-configured settings relating to brightness of elements should be ignored
|
||||
|
@ -19,6 +19,11 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString ShowHitMarkers => new TranslatableString(getKey(@"show_hit_markers"), @"Show hit markers");
|
||||
|
||||
/// <summary>
|
||||
/// "Automatically seek after placing objects"
|
||||
/// </summary>
|
||||
public static LocalisableString AutoSeekOnPlacement => new TranslatableString(getKey(@"auto_seek_on_placement"), @"Automatically seek after placing objects");
|
||||
|
||||
/// <summary>
|
||||
/// "Timing"
|
||||
/// </summary>
|
||||
|
@ -49,7 +49,7 @@ namespace osu.Game.Overlays.Notifications
|
||||
}
|
||||
}
|
||||
|
||||
public string CompletionText { get; set; } = "Task has completed!";
|
||||
public LocalisableString CompletionText { get; set; } = "Task has completed!";
|
||||
|
||||
private float progress;
|
||||
|
||||
|
@ -5,40 +5,53 @@ using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Overlays.SkinEditor
|
||||
{
|
||||
public partial class SkinBlueprint : SelectionBlueprint<ISkinnableDrawable>
|
||||
public partial class SkinBlueprint : SelectionBlueprint<ISerialisableDrawable>
|
||||
{
|
||||
private Container box = null!;
|
||||
|
||||
private Container outlineBox = null!;
|
||||
|
||||
private AnchorOriginVisualiser anchorOriginVisualiser = null!;
|
||||
|
||||
private OsuSpriteText label = null!;
|
||||
|
||||
private Drawable drawable => (Drawable)Item;
|
||||
|
||||
protected override bool ShouldBeAlive => drawable.IsAlive && Item.IsPresent;
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
private Quad drawableQuad;
|
||||
|
||||
public SkinBlueprint(ISkinnableDrawable component)
|
||||
public override Quad ScreenSpaceDrawQuad => drawableQuad;
|
||||
public override Quad SelectionQuad => drawable.ScreenSpaceDrawQuad;
|
||||
|
||||
public override bool Contains(Vector2 screenSpacePos) => drawableQuad.Contains(screenSpacePos);
|
||||
|
||||
public override Vector2 ScreenSpaceSelectionPoint => drawable.ToScreenSpace(drawable.OriginPosition);
|
||||
|
||||
protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) =>
|
||||
drawableQuad.Contains(screenSpacePos);
|
||||
|
||||
public SkinBlueprint(ISerialisableDrawable component)
|
||||
: base(component)
|
||||
{
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
@ -46,23 +59,26 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
outlineBox = new Container
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
BorderThickness = 3,
|
||||
BorderColour = Color4.White,
|
||||
CornerRadius = 3,
|
||||
BorderThickness = SelectionBox.BORDER_RADIUS / 2,
|
||||
BorderColour = ColourInfo.GradientVertical(colours.Pink4.Darken(0.4f), colours.Pink4),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0f,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Alpha = 0.2f,
|
||||
Colour = ColourInfo.GradientVertical(colours.Pink2, colours.Pink4),
|
||||
AlwaysPresent = true,
|
||||
},
|
||||
}
|
||||
},
|
||||
new OsuSpriteText
|
||||
label = new OsuSpriteText
|
||||
{
|
||||
Text = Item.GetType().Name,
|
||||
Font = OsuFont.Default.With(size: 10, weight: FontWeight.Bold),
|
||||
@ -86,6 +102,18 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
this.FadeInFromZero(200, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
updateSelectedState();
|
||||
return base.OnHover(e);
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
updateSelectedState();
|
||||
base.OnHoverLost(e);
|
||||
}
|
||||
|
||||
protected override void OnSelected()
|
||||
{
|
||||
// base logic hides selected blueprints when not selected, but skin blueprints don't do that.
|
||||
@ -100,73 +128,73 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
|
||||
private void updateSelectedState()
|
||||
{
|
||||
outlineBox.FadeColour(colours.Pink.Opacity(IsSelected ? 1 : 0.5f), 200, Easing.OutQuint);
|
||||
outlineBox.Child.FadeTo(IsSelected ? 0.2f : 0, 200, Easing.OutQuint);
|
||||
|
||||
anchorOriginVisualiser.FadeTo(IsSelected ? 1 : 0, 200, Easing.OutQuint);
|
||||
label.FadeTo(IsSelected || IsHovered ? 1 : 0, 200, Easing.OutQuint);
|
||||
}
|
||||
|
||||
private Quad drawableQuad;
|
||||
|
||||
public override Quad ScreenSpaceDrawQuad => drawableQuad;
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
drawableQuad = drawable.ScreenSpaceDrawQuad;
|
||||
var quad = ToLocalSpace(drawable.ScreenSpaceDrawQuad);
|
||||
drawableQuad = drawable.ToScreenSpace(
|
||||
drawable.DrawRectangle
|
||||
.Inflate(SkinSelectionHandler.INFLATE_SIZE));
|
||||
|
||||
box.Position = drawable.ToSpaceOfOtherDrawable(Vector2.Zero, this);
|
||||
box.Size = quad.Size;
|
||||
var localSpaceQuad = ToLocalSpace(drawableQuad);
|
||||
|
||||
box.Position = localSpaceQuad.TopLeft;
|
||||
box.Size = localSpaceQuad.Size;
|
||||
box.Rotation = drawable.Rotation;
|
||||
box.Scale = new Vector2(MathF.Sign(drawable.Scale.X), MathF.Sign(drawable.Scale.Y));
|
||||
}
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => drawable.ReceivePositionalInputAt(screenSpacePos);
|
||||
|
||||
public override Vector2 ScreenSpaceSelectionPoint => drawable.ToScreenSpace(drawable.OriginPosition);
|
||||
|
||||
public override Quad SelectionQuad => drawable.ScreenSpaceDrawQuad;
|
||||
}
|
||||
|
||||
internal partial class AnchorOriginVisualiser : CompositeDrawable
|
||||
{
|
||||
private readonly Drawable drawable;
|
||||
|
||||
private readonly Box originBox;
|
||||
private Drawable originBox = null!;
|
||||
|
||||
private readonly Box anchorBox;
|
||||
private readonly Box anchorLine;
|
||||
private Drawable anchorBox = null!;
|
||||
private Drawable anchorLine = null!;
|
||||
|
||||
public AnchorOriginVisualiser(Drawable drawable)
|
||||
{
|
||||
this.drawable = drawable;
|
||||
}
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
anchorLine = new Box
|
||||
Color4 anchorColour = colours.Red1;
|
||||
Color4 originColour = colours.Red3;
|
||||
|
||||
InternalChildren = new[]
|
||||
{
|
||||
Height = 2,
|
||||
anchorLine = new Circle
|
||||
{
|
||||
Height = 3f,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Colour = Color4.Yellow,
|
||||
EdgeSmoothness = Vector2.One
|
||||
Colour = ColourInfo.GradientHorizontal(originColour.Opacity(0.5f), originColour),
|
||||
},
|
||||
originBox = new Box
|
||||
originBox = new Circle
|
||||
{
|
||||
Colour = Color4.Red,
|
||||
Colour = originColour,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(5),
|
||||
Size = new Vector2(7),
|
||||
},
|
||||
anchorBox = new Box
|
||||
anchorBox = new Circle
|
||||
{
|
||||
Colour = Color4.Red,
|
||||
Colour = anchorColour,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(5),
|
||||
Size = new Vector2(10),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private Vector2? anchorPosition;
|
||||
private Vector2? originPositionInDrawableSpace;
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
@ -174,8 +202,13 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
if (drawable.Parent == null)
|
||||
return;
|
||||
|
||||
originBox.Position = drawable.ToSpaceOfOtherDrawable(drawable.OriginPosition, this);
|
||||
anchorBox.Position = drawable.Parent.ToSpaceOfOtherDrawable(drawable.AnchorPosition, this);
|
||||
var newAnchor = drawable.Parent.ToSpaceOfOtherDrawable(drawable.AnchorPosition, this);
|
||||
anchorPosition = tweenPosition(anchorPosition ?? newAnchor, newAnchor);
|
||||
anchorBox.Position = anchorPosition.Value;
|
||||
|
||||
// for the origin, tween in the drawable's local space to avoid unwanted tweening when the drawable is being dragged.
|
||||
originPositionInDrawableSpace = originPositionInDrawableSpace != null ? tweenPosition(originPositionInDrawableSpace.Value, drawable.OriginPosition) : drawable.OriginPosition;
|
||||
originBox.Position = drawable.ToSpaceOfOtherDrawable(originPositionInDrawableSpace.Value, this);
|
||||
|
||||
var point1 = ToLocalSpace(anchorBox.ScreenSpaceDrawQuad.Centre);
|
||||
var point2 = ToLocalSpace(originBox.ScreenSpaceDrawQuad.Centre);
|
||||
@ -184,5 +217,11 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
anchorLine.Width = (point2 - point1).Length;
|
||||
anchorLine.Rotation = MathHelper.RadiansToDegrees(MathF.Atan2(point2.Y - point1.Y, point2.X - point1.X));
|
||||
}
|
||||
|
||||
private Vector2 tweenPosition(Vector2 oldPosition, Vector2 newPosition)
|
||||
=> new Vector2(
|
||||
(float)Interpolation.DampContinuously(oldPosition.X, newPosition.X, 25, Clock.ElapsedFrameTime),
|
||||
(float)Interpolation.DampContinuously(oldPosition.Y, newPosition.Y, 25, Clock.ElapsedFrameTime)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -24,11 +24,11 @@ using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Overlays.SkinEditor
|
||||
{
|
||||
public partial class SkinBlueprintContainer : BlueprintContainer<ISkinnableDrawable>
|
||||
public partial class SkinBlueprintContainer : BlueprintContainer<ISerialisableDrawable>
|
||||
{
|
||||
private readonly Drawable target;
|
||||
|
||||
private readonly List<BindableList<ISkinnableDrawable>> targetComponents = new List<BindableList<ISkinnableDrawable>>();
|
||||
private readonly List<BindableList<ISerialisableDrawable>> targetComponents = new List<BindableList<ISerialisableDrawable>>();
|
||||
|
||||
[Resolved]
|
||||
private SkinEditor editor { get; set; } = null!;
|
||||
@ -45,7 +45,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
SelectedItems.BindTo(editor.SelectedComponents);
|
||||
|
||||
// track each target container on the current screen.
|
||||
var targetContainers = target.ChildrenOfType<ISkinnableTarget>().ToArray();
|
||||
var targetContainers = target.ChildrenOfType<ISerialisableDrawableContainer>().ToArray();
|
||||
|
||||
if (targetContainers.Length == 0)
|
||||
{
|
||||
@ -55,7 +55,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
|
||||
foreach (var targetContainer in targetContainers)
|
||||
{
|
||||
var bindableList = new BindableList<ISkinnableDrawable> { BindTarget = targetContainer.Components };
|
||||
var bindableList = new BindableList<ISerialisableDrawable> { BindTarget = targetContainer.Components };
|
||||
bindableList.BindCollectionChanged(componentsChanged, true);
|
||||
|
||||
targetComponents.Add(bindableList);
|
||||
@ -69,7 +69,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
Debug.Assert(e.NewItems != null);
|
||||
|
||||
foreach (var item in e.NewItems.Cast<ISkinnableDrawable>())
|
||||
foreach (var item in e.NewItems.Cast<ISerialisableDrawable>())
|
||||
AddBlueprintFor(item);
|
||||
break;
|
||||
|
||||
@ -77,7 +77,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
case NotifyCollectionChangedAction.Reset:
|
||||
Debug.Assert(e.OldItems != null);
|
||||
|
||||
foreach (var item in e.OldItems.Cast<ISkinnableDrawable>())
|
||||
foreach (var item in e.OldItems.Cast<ISerialisableDrawable>())
|
||||
RemoveBlueprintFor(item);
|
||||
break;
|
||||
|
||||
@ -85,16 +85,16 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
Debug.Assert(e.NewItems != null);
|
||||
Debug.Assert(e.OldItems != null);
|
||||
|
||||
foreach (var item in e.OldItems.Cast<ISkinnableDrawable>())
|
||||
foreach (var item in e.OldItems.Cast<ISerialisableDrawable>())
|
||||
RemoveBlueprintFor(item);
|
||||
|
||||
foreach (var item in e.NewItems.Cast<ISkinnableDrawable>())
|
||||
foreach (var item in e.NewItems.Cast<ISerialisableDrawable>())
|
||||
AddBlueprintFor(item);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
protected override void AddBlueprintFor(ISkinnableDrawable item)
|
||||
protected override void AddBlueprintFor(ISerialisableDrawable item)
|
||||
{
|
||||
if (!item.IsEditable)
|
||||
return;
|
||||
@ -145,12 +145,12 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
// convert to game space coordinates
|
||||
delta = firstBlueprint.ToScreenSpace(delta) - firstBlueprint.ToScreenSpace(Vector2.Zero);
|
||||
|
||||
SelectionHandler.HandleMovement(new MoveSelectionEvent<ISkinnableDrawable>(firstBlueprint, delta));
|
||||
SelectionHandler.HandleMovement(new MoveSelectionEvent<ISerialisableDrawable>(firstBlueprint, delta));
|
||||
}
|
||||
|
||||
protected override SelectionHandler<ISkinnableDrawable> CreateSelectionHandler() => new SkinSelectionHandler();
|
||||
protected override SelectionHandler<ISerialisableDrawable> CreateSelectionHandler() => new SkinSelectionHandler();
|
||||
|
||||
protected override SelectionBlueprint<ISkinnableDrawable> CreateBlueprintFor(ISkinnableDrawable component)
|
||||
protected override SelectionBlueprint<ISerialisableDrawable> CreateBlueprintFor(ISerialisableDrawable component)
|
||||
=> new SkinBlueprint(component);
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
|
@ -2,17 +2,17 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Screens.Edit.Components;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
@ -50,7 +50,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
{
|
||||
fill.Clear();
|
||||
|
||||
var skinnableTypes = SkinnableInfo.GetAllAvailableDrawables();
|
||||
var skinnableTypes = SerialisedDrawableInfo.GetAllAvailableDrawables();
|
||||
foreach (var type in skinnableTypes)
|
||||
attemptAddComponent(type);
|
||||
}
|
||||
@ -61,11 +61,12 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
{
|
||||
Drawable instance = (Drawable)Activator.CreateInstance(type)!;
|
||||
|
||||
if (!((ISkinnableDrawable)instance).IsEditable) return;
|
||||
if (!((ISerialisableDrawable)instance).IsEditable) return;
|
||||
|
||||
fill.Add(new ToolboxComponentButton(instance, target)
|
||||
{
|
||||
RequestPlacement = t => RequestPlacement?.Invoke(t)
|
||||
RequestPlacement = t => RequestPlacement?.Invoke(t),
|
||||
Expanding = contractOtherButtons,
|
||||
});
|
||||
}
|
||||
catch (DependencyNotRegisteredException)
|
||||
@ -79,15 +80,29 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
}
|
||||
}
|
||||
|
||||
private void contractOtherButtons(ToolboxComponentButton obj)
|
||||
{
|
||||
foreach (var b in fill.OfType<ToolboxComponentButton>())
|
||||
{
|
||||
if (b == obj)
|
||||
continue;
|
||||
|
||||
b.Contract();
|
||||
}
|
||||
}
|
||||
|
||||
public partial class ToolboxComponentButton : OsuButton
|
||||
{
|
||||
public Action<Type>? RequestPlacement;
|
||||
public Action<ToolboxComponentButton>? Expanding;
|
||||
|
||||
private readonly Drawable component;
|
||||
private readonly CompositeDrawable? dependencySource;
|
||||
|
||||
private Container innerContainer = null!;
|
||||
|
||||
private ScheduledDelegate? expandContractAction;
|
||||
|
||||
private const float contracted_size = 60;
|
||||
private const float expanded_size = 120;
|
||||
|
||||
@ -102,20 +117,45 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
Height = contracted_size;
|
||||
}
|
||||
|
||||
private const double animation_duration = 500;
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
this.Delay(300).ResizeHeightTo(expanded_size, 500, Easing.OutQuint);
|
||||
expandContractAction?.Cancel();
|
||||
expandContractAction = Scheduler.AddDelayed(() =>
|
||||
{
|
||||
this.ResizeHeightTo(expanded_size, animation_duration, Easing.OutQuint);
|
||||
Expanding?.Invoke(this);
|
||||
}, 100);
|
||||
|
||||
return base.OnHover(e);
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
base.OnHoverLost(e);
|
||||
this.ResizeHeightTo(contracted_size, 500, Easing.OutQuint);
|
||||
|
||||
expandContractAction?.Cancel();
|
||||
// If no other component is selected for too long, force a contract.
|
||||
// Otherwise we will generally contract when Contract() is called from outside.
|
||||
expandContractAction = Scheduler.AddDelayed(Contract, 1000);
|
||||
}
|
||||
|
||||
public void Contract()
|
||||
{
|
||||
// Cheap debouncing to avoid stacking animations.
|
||||
// The only place this is nulled is at the end of this method.
|
||||
if (expandContractAction == null)
|
||||
return;
|
||||
|
||||
this.ResizeHeightTo(contracted_size, animation_duration, Easing.OutQuint);
|
||||
|
||||
expandContractAction?.Cancel();
|
||||
expandContractAction = null;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider, OsuColour colours)
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
BackgroundColour = colourProvider.Background3;
|
||||
|
||||
|
@ -38,7 +38,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
|
||||
public const float MENU_HEIGHT = 40;
|
||||
|
||||
public readonly BindableList<ISkinnableDrawable> SelectedComponents = new BindableList<ISkinnableDrawable>();
|
||||
public readonly BindableList<ISerialisableDrawable> SelectedComponents = new BindableList<ISerialisableDrawable>();
|
||||
|
||||
protected override bool StartHidden => true;
|
||||
|
||||
@ -302,13 +302,13 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
|
||||
private void placeComponent(Type type)
|
||||
{
|
||||
if (!(Activator.CreateInstance(type) is ISkinnableDrawable component))
|
||||
throw new InvalidOperationException($"Attempted to instantiate a component for placement which was not an {typeof(ISkinnableDrawable)}.");
|
||||
if (!(Activator.CreateInstance(type) is ISerialisableDrawable component))
|
||||
throw new InvalidOperationException($"Attempted to instantiate a component for placement which was not an {typeof(ISerialisableDrawable)}.");
|
||||
|
||||
placeComponent(component);
|
||||
}
|
||||
|
||||
private void placeComponent(ISkinnableDrawable component, bool applyDefaults = true)
|
||||
private void placeComponent(ISerialisableDrawable component, bool applyDefaults = true)
|
||||
{
|
||||
var targetContainer = getFirstTarget();
|
||||
|
||||
@ -339,25 +339,25 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
settingsSidebar.Add(new SkinSettingsToolbox(component));
|
||||
}
|
||||
|
||||
private IEnumerable<ISkinnableTarget> availableTargets => targetScreen.ChildrenOfType<ISkinnableTarget>();
|
||||
private IEnumerable<SkinComponentsContainer> availableTargets => targetScreen.ChildrenOfType<SkinComponentsContainer>();
|
||||
|
||||
private ISkinnableTarget? getFirstTarget() => availableTargets.FirstOrDefault();
|
||||
private ISerialisableDrawableContainer? getFirstTarget() => availableTargets.FirstOrDefault();
|
||||
|
||||
private ISkinnableTarget? getTarget(GlobalSkinComponentLookup.LookupType target)
|
||||
private ISerialisableDrawableContainer? getTarget(SkinComponentsContainerLookup.TargetArea target)
|
||||
{
|
||||
return availableTargets.FirstOrDefault(c => c.Target == target);
|
||||
return availableTargets.FirstOrDefault(c => c.Lookup.Target == target);
|
||||
}
|
||||
|
||||
private void revert()
|
||||
{
|
||||
ISkinnableTarget[] targetContainers = availableTargets.ToArray();
|
||||
SkinComponentsContainer[] targetContainers = availableTargets.ToArray();
|
||||
|
||||
foreach (var t in targetContainers)
|
||||
{
|
||||
currentSkin.Value.ResetDrawableTarget(t);
|
||||
|
||||
// add back default components
|
||||
getTarget(t.Target)?.Reload();
|
||||
getTarget(t.Lookup.Target)?.Reload();
|
||||
}
|
||||
}
|
||||
|
||||
@ -370,7 +370,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
if (!hasBegunMutating)
|
||||
return;
|
||||
|
||||
ISkinnableTarget[] targetContainers = availableTargets.ToArray();
|
||||
SkinComponentsContainer[] targetContainers = availableTargets.ToArray();
|
||||
|
||||
foreach (var t in targetContainers)
|
||||
currentSkin.Value.UpdateDrawableTarget(t);
|
||||
@ -400,7 +400,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
this.FadeOut(TRANSITION_DURATION, Easing.OutQuint);
|
||||
}
|
||||
|
||||
public void DeleteItems(ISkinnableDrawable[] items)
|
||||
public void DeleteItems(ISerialisableDrawable[] items)
|
||||
{
|
||||
foreach (var item in items)
|
||||
availableTargets.FirstOrDefault(t => t.Components.Contains(item))?.Remove(item);
|
||||
|
@ -9,19 +9,17 @@ using Newtonsoft.Json;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Overlays.SkinEditor
|
||||
{
|
||||
public partial class SkinEditorChangeHandler : EditorChangeHandler
|
||||
{
|
||||
private readonly ISkinnableTarget? firstTarget;
|
||||
private readonly ISerialisableDrawableContainer? firstTarget;
|
||||
|
||||
// ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable
|
||||
private readonly BindableList<ISkinnableDrawable>? components;
|
||||
private readonly BindableList<ISerialisableDrawable>? components;
|
||||
|
||||
public SkinEditorChangeHandler(Drawable targetScreen)
|
||||
{
|
||||
@ -29,12 +27,12 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
// In the future we'll want this to cover all changes, even to skin's `InstantiationInfo`.
|
||||
// We'll also need to consider cases where multiple targets are on screen at the same time.
|
||||
|
||||
firstTarget = targetScreen.ChildrenOfType<ISkinnableTarget>().FirstOrDefault();
|
||||
firstTarget = targetScreen.ChildrenOfType<ISerialisableDrawableContainer>().FirstOrDefault();
|
||||
|
||||
if (firstTarget == null)
|
||||
return;
|
||||
|
||||
components = new BindableList<ISkinnableDrawable> { BindTarget = firstTarget.Components };
|
||||
components = new BindableList<ISerialisableDrawable> { BindTarget = firstTarget.Components };
|
||||
components.BindCollectionChanged((_, _) => SaveState());
|
||||
}
|
||||
|
||||
@ -43,7 +41,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
if (firstTarget == null)
|
||||
return;
|
||||
|
||||
var skinnableInfos = firstTarget.CreateSkinnableInfo().ToArray();
|
||||
var skinnableInfos = firstTarget.CreateSerialisedInfo().ToArray();
|
||||
string json = JsonConvert.SerializeObject(skinnableInfos, new JsonSerializerSettings { Formatting = Formatting.Indented });
|
||||
stream.Write(Encoding.UTF8.GetBytes(json));
|
||||
}
|
||||
@ -53,12 +51,12 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
if (firstTarget == null)
|
||||
return;
|
||||
|
||||
var deserializedContent = JsonConvert.DeserializeObject<IEnumerable<SkinnableInfo>>(Encoding.UTF8.GetString(newState));
|
||||
var deserializedContent = JsonConvert.DeserializeObject<IEnumerable<SerialisedDrawableInfo>>(Encoding.UTF8.GetString(newState));
|
||||
|
||||
if (deserializedContent == null)
|
||||
return;
|
||||
|
||||
SkinnableInfo[] skinnableInfo = deserializedContent.ToArray();
|
||||
SerialisedDrawableInfo[] skinnableInfo = deserializedContent.ToArray();
|
||||
Drawable[] targetComponents = firstTarget.Components.OfType<Drawable>().ToArray();
|
||||
|
||||
if (!skinnableInfo.Select(s => s.Type).SequenceEqual(targetComponents.Select(d => d.GetType())))
|
||||
@ -71,7 +69,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
int i = 0;
|
||||
|
||||
foreach (var drawable in targetComponents)
|
||||
drawable.ApplySkinnableInfo(skinnableInfo[i++]);
|
||||
drawable.ApplySerialisedInfo(skinnableInfo[i++]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Overlays.SkinEditor
|
||||
{
|
||||
public partial class SkinSelectionHandler : SelectionHandler<ISkinnableDrawable>
|
||||
public partial class SkinSelectionHandler : SelectionHandler<ISerialisableDrawable>
|
||||
{
|
||||
[Resolved]
|
||||
private SkinEditor skinEditor { get; set; } = null!;
|
||||
@ -147,7 +147,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool HandleMovement(MoveSelectionEvent<ISkinnableDrawable> moveEvent)
|
||||
public override bool HandleMovement(MoveSelectionEvent<ISerialisableDrawable> moveEvent)
|
||||
{
|
||||
foreach (var c in SelectedBlueprints)
|
||||
{
|
||||
@ -178,10 +178,10 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
SelectionBox.CanReverse = false;
|
||||
}
|
||||
|
||||
protected override void DeleteItems(IEnumerable<ISkinnableDrawable> items) =>
|
||||
protected override void DeleteItems(IEnumerable<ISerialisableDrawable> items) =>
|
||||
skinEditor.DeleteItems(items.ToArray());
|
||||
|
||||
protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint<ISkinnableDrawable>> selection)
|
||||
protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint<ISerialisableDrawable>> selection)
|
||||
{
|
||||
var closestItem = new TernaryStateRadioMenuItem("Closest", MenuItemType.Standard, _ => applyClosestAnchors())
|
||||
{
|
||||
@ -209,7 +209,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
foreach (var item in base.GetContextMenuItemsForSelection(selection))
|
||||
yield return item;
|
||||
|
||||
IEnumerable<TernaryStateMenuItem> createAnchorItems(Func<ISkinnableDrawable, Anchor, bool> checkFunction, Action<Anchor> applyFunction)
|
||||
IEnumerable<TernaryStateMenuItem> createAnchorItems(Func<ISerialisableDrawable, Anchor, bool> checkFunction, Action<Anchor> applyFunction)
|
||||
{
|
||||
var displayableAnchors = new[]
|
||||
{
|
||||
|
@ -17,6 +17,7 @@ using osu.Framework.Input;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets.Configuration;
|
||||
using osu.Game.Rulesets.Edit.Tools;
|
||||
@ -70,6 +71,7 @@ namespace osu.Game.Rulesets.Edit
|
||||
private FillFlowContainer togglesCollection;
|
||||
|
||||
private IBindable<bool> hasTiming;
|
||||
private Bindable<bool> autoSeekOnPlacement;
|
||||
|
||||
protected HitObjectComposer(Ruleset ruleset)
|
||||
: base(ruleset)
|
||||
@ -80,8 +82,10 @@ namespace osu.Game.Rulesets.Edit
|
||||
dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
private void load(OverlayColourProvider colourProvider, OsuConfigManager config)
|
||||
{
|
||||
autoSeekOnPlacement = config.GetBindable<bool>(OsuSetting.EditorAutoSeekOnPlacement);
|
||||
|
||||
Config = Dependencies.Get<IRulesetConfigCache>().GetConfigFor(Ruleset);
|
||||
|
||||
try
|
||||
@ -365,7 +369,7 @@ namespace osu.Game.Rulesets.Edit
|
||||
{
|
||||
EditorBeatmap.Add(hitObject);
|
||||
|
||||
if (EditorClock.CurrentTime < hitObject.StartTime)
|
||||
if (autoSeekOnPlacement.Value && EditorClock.CurrentTime < hitObject.StartTime)
|
||||
EditorClock.SeekSmoothlyTo(hitObject.StartTime);
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
@ -33,16 +34,30 @@ namespace osu.Game.Rulesets.Judgements
|
||||
public readonly Judgement Judgement;
|
||||
|
||||
/// <summary>
|
||||
/// The offset from a perfect hit at which this <see cref="JudgementResult"/> occurred.
|
||||
/// The time at which this <see cref="JudgementResult"/> occurred.
|
||||
/// Populated when this <see cref="JudgementResult"/> is applied via <see cref="DrawableHitObject.ApplyResult"/>.
|
||||
/// </summary>
|
||||
public double TimeOffset { get; internal set; }
|
||||
/// <remarks>
|
||||
/// This is used instead of <see cref="TimeAbsolute"/> to check whether this <see cref="JudgementResult"/> should be reverted.
|
||||
/// </remarks>
|
||||
internal double? RawTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The absolute time at which this <see cref="JudgementResult"/> occurred.
|
||||
/// Equal to the (end) time of the <see cref="HitObject"/> + <see cref="TimeOffset"/>.
|
||||
/// The offset of <see cref="TimeAbsolute"/> from the end time of <see cref="HitObject"/>, clamped by <see cref="osu.Game.Rulesets.Objects.HitObject.MaximumJudgementOffset"/>.
|
||||
/// </summary>
|
||||
public double TimeAbsolute => HitObject.GetEndTime() + TimeOffset;
|
||||
public double TimeOffset
|
||||
{
|
||||
get => RawTime != null ? Math.Min(RawTime.Value - HitObject.GetEndTime(), HitObject.MaximumJudgementOffset) : 0;
|
||||
internal set => RawTime = HitObject.GetEndTime() + value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The absolute time at which this <see cref="JudgementResult"/> occurred, clamped by the end time of <see cref="HitObject"/> plus <see cref="osu.Game.Rulesets.Objects.HitObject.MaximumJudgementOffset"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The end time of <see cref="HitObject"/> is returned if this result is not populated yet.
|
||||
/// </remarks>
|
||||
public double TimeAbsolute => RawTime != null ? Math.Min(RawTime.Value, HitObject.GetEndTime() + HitObject.MaximumJudgementOffset) : HitObject.GetEndTime();
|
||||
|
||||
/// <summary>
|
||||
/// The combo prior to this <see cref="JudgementResult"/> occurring.
|
||||
@ -83,6 +98,13 @@ namespace osu.Game.Rulesets.Judgements
|
||||
{
|
||||
HitObject = hitObject;
|
||||
Judgement = judgement;
|
||||
Reset();
|
||||
}
|
||||
|
||||
internal void Reset()
|
||||
{
|
||||
Type = HitResult.None;
|
||||
RawTime = null;
|
||||
}
|
||||
|
||||
public override string ToString() => $"{Type} (Score:{Judgement.NumericResultFor(this)} HP:{Judgement.HealthIncreaseFor(this)} {Judgement})";
|
||||
|
@ -82,6 +82,9 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
/// <summary>
|
||||
/// Invoked by this or a nested <see cref="DrawableHitObject"/> prior to a <see cref="JudgementResult"/> being reverted.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is only invoked if this <see cref="DrawableHitObject"/> is alive when the result is reverted.
|
||||
/// </remarks>
|
||||
public event Action<DrawableHitObject, JudgementResult> OnRevertResult;
|
||||
|
||||
/// <summary>
|
||||
@ -222,6 +225,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
|
||||
ensureEntryHasResult();
|
||||
|
||||
entry.RevertResult += onRevertResult;
|
||||
|
||||
foreach (var h in HitObject.NestedHitObjects)
|
||||
{
|
||||
var pooledDrawableNested = pooledObjectProvider?.GetPooledDrawableRepresentation(h, this);
|
||||
@ -234,7 +239,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
OnNestedDrawableCreated?.Invoke(drawableNested);
|
||||
|
||||
drawableNested.OnNewResult += onNewResult;
|
||||
drawableNested.OnRevertResult += onRevertResult;
|
||||
drawableNested.ApplyCustomUpdateState += onApplyCustomUpdateState;
|
||||
|
||||
// This is only necessary for non-pooled DHOs. For pooled DHOs, this is handled inside GetPooledDrawableRepresentation().
|
||||
@ -308,7 +312,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
foreach (var obj in nestedHitObjects)
|
||||
{
|
||||
obj.OnNewResult -= onNewResult;
|
||||
obj.OnRevertResult -= onRevertResult;
|
||||
obj.ApplyCustomUpdateState -= onApplyCustomUpdateState;
|
||||
}
|
||||
|
||||
@ -317,6 +320,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
|
||||
HitObject.DefaultsApplied -= onDefaultsApplied;
|
||||
|
||||
entry.RevertResult -= onRevertResult;
|
||||
|
||||
OnFree();
|
||||
|
||||
ParentHitObject = null;
|
||||
@ -365,7 +370,11 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
|
||||
private void onNewResult(DrawableHitObject drawableHitObject, JudgementResult result) => OnNewResult?.Invoke(drawableHitObject, result);
|
||||
|
||||
private void onRevertResult(DrawableHitObject drawableHitObject, JudgementResult result) => OnRevertResult?.Invoke(drawableHitObject, result);
|
||||
private void onRevertResult()
|
||||
{
|
||||
updateState(ArmedState.Idle);
|
||||
OnRevertResult?.Invoke(this, Result);
|
||||
}
|
||||
|
||||
private void onApplyCustomUpdateState(DrawableHitObject drawableHitObject, ArmedState state) => ApplyCustomUpdateState?.Invoke(drawableHitObject, state);
|
||||
|
||||
@ -577,26 +586,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
|
||||
#endregion
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (Result != null && Result.HasResult)
|
||||
{
|
||||
double endTime = HitObject.GetEndTime();
|
||||
|
||||
if (Result.TimeOffset + endTime > Time.Current)
|
||||
{
|
||||
OnRevertResult?.Invoke(this, Result);
|
||||
|
||||
Result.TimeOffset = 0;
|
||||
Result.Type = HitResult.None;
|
||||
|
||||
updateState(ArmedState.Idle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) => false;
|
||||
|
||||
protected override void UpdateAfterChildren()
|
||||
@ -650,18 +639,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
UpdateResult(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The maximum offset from the end time of <see cref="HitObject"/> at which this <see cref="DrawableHitObject"/> can be judged.
|
||||
/// The time offset of <see cref="Result"/> will be clamped to this value during <see cref="ApplyResult"/>.
|
||||
/// <para>
|
||||
/// Defaults to the miss window of <see cref="HitObject"/>.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This does not affect the time offset provided to invocations of <see cref="CheckForResult"/>.
|
||||
/// </remarks>
|
||||
public virtual double MaximumJudgementOffset => HitObject.HitWindows?.WindowFor(HitResult.Miss) ?? 0;
|
||||
|
||||
/// <summary>
|
||||
/// Applies the <see cref="Result"/> of this <see cref="DrawableHitObject"/>, notifying responders such as
|
||||
/// the <see cref="ScoreProcessor"/> of the <see cref="JudgementResult"/>.
|
||||
@ -683,7 +660,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
$"{GetType().ReadableName()} applied an invalid hit result (was: {Result.Type}, expected: [{Result.Judgement.MinResult} ... {Result.Judgement.MaxResult}]).");
|
||||
}
|
||||
|
||||
Result.TimeOffset = Math.Min(MaximumJudgementOffset, Time.Current - HitObject.GetEndTime());
|
||||
Result.RawTime = Time.Current;
|
||||
|
||||
if (Result.HasResult)
|
||||
updateState(Result.IsHit ? ArmedState.Hit : ArmedState.Miss);
|
||||
|
@ -200,6 +200,14 @@ namespace osu.Game.Rulesets.Objects
|
||||
[NotNull]
|
||||
protected virtual HitWindows CreateHitWindows() => new HitWindows();
|
||||
|
||||
/// <summary>
|
||||
/// The maximum offset from the end time of <see cref="HitObject"/> at which this <see cref="HitObject"/> can be judged.
|
||||
/// <para>
|
||||
/// Defaults to the miss window.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public virtual double MaximumJudgementOffset => HitWindows?.WindowFor(HitResult.Miss) ?? 0;
|
||||
|
||||
public IList<HitSampleInfo> CreateSlidingSamples()
|
||||
{
|
||||
var slidingSamples = new List<HitSampleInfo>();
|
||||
|
@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Performance;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
@ -26,6 +27,8 @@ namespace osu.Game.Rulesets.Objects
|
||||
|
||||
private readonly IBindable<double> startTimeBindable = new BindableDouble();
|
||||
|
||||
internal event Action? RevertResult;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="HitObjectLifetimeEntry"/>.
|
||||
/// </summary>
|
||||
@ -95,5 +98,7 @@ namespace osu.Game.Rulesets.Objects
|
||||
/// Set <see cref="LifetimeEntry.LifetimeStart"/> using <see cref="InitialLifetimeOffset"/>.
|
||||
/// </summary>
|
||||
internal void SetInitialLifetime() => LifetimeStart = HitObject.StartTime - InitialLifetimeOffset;
|
||||
|
||||
internal void OnRevertResult() => RevertResult?.Invoke();
|
||||
}
|
||||
}
|
||||
|
@ -134,7 +134,7 @@ namespace osu.Game.Rulesets.UI
|
||||
playfield = new Lazy<Playfield>(() => CreatePlayfield().With(p =>
|
||||
{
|
||||
p.NewResult += (_, r) => NewResult?.Invoke(r);
|
||||
p.RevertResult += (_, r) => RevertResult?.Invoke(r);
|
||||
p.RevertResult += r => RevertResult?.Invoke(r);
|
||||
}));
|
||||
}
|
||||
|
||||
@ -230,7 +230,7 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
public override void RequestResume(Action continueResume)
|
||||
{
|
||||
if (ResumeOverlay != null && (Cursor == null || (Cursor.LastFrameState == Visibility.Visible && Contains(Cursor.ActiveCursor.ScreenSpaceDrawQuad.Centre))))
|
||||
if (ResumeOverlay != null && UseResumeOverlay && (Cursor == null || (Cursor.LastFrameState == Visibility.Visible && Contains(Cursor.ActiveCursor.ScreenSpaceDrawQuad.Centre))))
|
||||
{
|
||||
ResumeOverlay.GameplayCursor = Cursor;
|
||||
ResumeOverlay.ResumeAction = continueResume;
|
||||
@ -507,6 +507,15 @@ namespace osu.Game.Rulesets.UI
|
||||
/// </summary>
|
||||
public ResumeOverlay ResumeOverlay { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the <see cref="ResumeOverlay"/> should be used to return the user's cursor position to its previous location after a pause.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Defaults to <c>true</c>.
|
||||
/// Even if <c>true</c>, will not have any effect if the ruleset does not have a resume overlay (see <see cref="CreateResumeOverlay"/>).
|
||||
/// </remarks>
|
||||
public bool UseResumeOverlay { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Returns first available <see cref="HitWindows"/> provided by a <see cref="HitObject"/>.
|
||||
/// </summary>
|
||||
@ -531,6 +540,11 @@ namespace osu.Game.Rulesets.UI
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an optional resume overlay, which is displayed when a player requests to resume gameplay during non-break time.
|
||||
/// This can be used to force the player to return their hands / cursor to the position they left off, to avoid players
|
||||
/// using pauses as a means of adjusting their inputs (aka "pause buffering").
|
||||
/// </summary>
|
||||
protected virtual ResumeOverlay CreateResumeOverlay() => null;
|
||||
|
||||
/// <summary>
|
||||
|
@ -28,11 +28,6 @@ namespace osu.Game.Rulesets.UI
|
||||
/// </summary>
|
||||
public event Action<DrawableHitObject, JudgementResult> NewResult;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a <see cref="DrawableHitObject"/> judgement is reverted.
|
||||
/// </summary>
|
||||
public event Action<DrawableHitObject, JudgementResult> RevertResult;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a <see cref="HitObject"/> becomes used by a <see cref="DrawableHitObject"/>.
|
||||
/// </summary>
|
||||
@ -111,7 +106,6 @@ namespace osu.Game.Rulesets.UI
|
||||
private void addDrawable(DrawableHitObject drawable)
|
||||
{
|
||||
drawable.OnNewResult += onNewResult;
|
||||
drawable.OnRevertResult += onRevertResult;
|
||||
|
||||
bindStartTime(drawable);
|
||||
AddInternal(drawable);
|
||||
@ -120,7 +114,6 @@ namespace osu.Game.Rulesets.UI
|
||||
private void removeDrawable(DrawableHitObject drawable)
|
||||
{
|
||||
drawable.OnNewResult -= onNewResult;
|
||||
drawable.OnRevertResult -= onRevertResult;
|
||||
|
||||
unbindStartTime(drawable);
|
||||
|
||||
@ -154,7 +147,6 @@ namespace osu.Game.Rulesets.UI
|
||||
#endregion
|
||||
|
||||
private void onNewResult(DrawableHitObject d, JudgementResult r) => NewResult?.Invoke(d, r);
|
||||
private void onRevertResult(DrawableHitObject d, JudgementResult r) => RevertResult?.Invoke(d, r);
|
||||
|
||||
#region Comparator + StartTime tracking
|
||||
|
||||
|
@ -22,6 +22,7 @@ using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using osu.Game.Rulesets.Objects.Pooling;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
|
||||
namespace osu.Game.Rulesets.UI
|
||||
{
|
||||
@ -35,9 +36,9 @@ namespace osu.Game.Rulesets.UI
|
||||
public event Action<DrawableHitObject, JudgementResult> NewResult;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a <see cref="DrawableHitObject"/> judgement is reverted.
|
||||
/// Invoked when a judgement result is reverted.
|
||||
/// </summary>
|
||||
public event Action<DrawableHitObject, JudgementResult> RevertResult;
|
||||
public event Action<JudgementResult> RevertResult;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="DrawableHitObject"/> contained in this Playfield.
|
||||
@ -98,6 +99,8 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
private readonly HitObjectEntryManager entryManager = new HitObjectEntryManager();
|
||||
|
||||
private readonly Stack<HitObjectLifetimeEntry> judgedEntries;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="Playfield"/>.
|
||||
/// </summary>
|
||||
@ -107,14 +110,15 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
hitObjectContainerLazy = new Lazy<HitObjectContainer>(() => CreateHitObjectContainer().With(h =>
|
||||
{
|
||||
h.NewResult += (d, r) => NewResult?.Invoke(d, r);
|
||||
h.RevertResult += (d, r) => RevertResult?.Invoke(d, r);
|
||||
h.NewResult += onNewResult;
|
||||
h.HitObjectUsageBegan += o => HitObjectUsageBegan?.Invoke(o);
|
||||
h.HitObjectUsageFinished += o => HitObjectUsageFinished?.Invoke(o);
|
||||
}));
|
||||
|
||||
entryManager.OnEntryAdded += onEntryAdded;
|
||||
entryManager.OnEntryRemoved += onEntryRemoved;
|
||||
|
||||
judgedEntries = new Stack<HitObjectLifetimeEntry>();
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@ -224,7 +228,7 @@ namespace osu.Game.Rulesets.UI
|
||||
otherPlayfield.DisplayJudgements.BindTo(DisplayJudgements);
|
||||
|
||||
otherPlayfield.NewResult += (d, r) => NewResult?.Invoke(d, r);
|
||||
otherPlayfield.RevertResult += (d, r) => RevertResult?.Invoke(d, r);
|
||||
otherPlayfield.RevertResult += r => RevertResult?.Invoke(r);
|
||||
otherPlayfield.HitObjectUsageBegan += h => HitObjectUsageBegan?.Invoke(h);
|
||||
otherPlayfield.HitObjectUsageFinished += h => HitObjectUsageFinished?.Invoke(h);
|
||||
|
||||
@ -252,6 +256,18 @@ namespace osu.Game.Rulesets.UI
|
||||
updatable.Update(this);
|
||||
}
|
||||
}
|
||||
|
||||
// When rewinding, revert future judgements in the reverse order.
|
||||
while (judgedEntries.Count > 0)
|
||||
{
|
||||
var result = judgedEntries.Peek().Result;
|
||||
Debug.Assert(result?.RawTime != null);
|
||||
|
||||
if (Time.Current >= result.RawTime.Value)
|
||||
break;
|
||||
|
||||
revertResult(judgedEntries.Pop());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -443,6 +459,25 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
#endregion
|
||||
|
||||
private void onNewResult(DrawableHitObject drawable, JudgementResult result)
|
||||
{
|
||||
Debug.Assert(result != null && drawable.Entry?.Result == result && result.RawTime != null);
|
||||
judgedEntries.Push(drawable.Entry.AsNonNull());
|
||||
|
||||
NewResult?.Invoke(drawable, result);
|
||||
}
|
||||
|
||||
private void revertResult(HitObjectLifetimeEntry entry)
|
||||
{
|
||||
var result = entry.Result;
|
||||
Debug.Assert(result != null);
|
||||
|
||||
RevertResult?.Invoke(result);
|
||||
entry.OnRevertResult();
|
||||
|
||||
result.Reset();
|
||||
}
|
||||
|
||||
#region Editor logic
|
||||
|
||||
/// <summary>
|
||||
|
@ -232,8 +232,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
double computedStartTime = computeDisplayStartTime(entry);
|
||||
|
||||
// always load the hitobject before its first judgement offset
|
||||
double judgementOffset = entry.HitObject.HitWindows?.WindowFor(Scoring.HitResult.Miss) ?? 0;
|
||||
entry.LifetimeStart = Math.Min(entry.HitObject.StartTime - judgementOffset, computedStartTime);
|
||||
entry.LifetimeStart = Math.Min(entry.HitObject.StartTime - entry.HitObject.MaximumJudgementOffset, computedStartTime);
|
||||
}
|
||||
|
||||
private void updateLayoutRecursive(DrawableHitObject hitObject, double? parentHitObjectStartTime = null)
|
||||
|
@ -32,6 +32,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
/// </summary>
|
||||
public abstract partial class SelectionHandler<T> : CompositeDrawable, IKeyBindingHandler<PlatformAction>, IKeyBindingHandler<GlobalAction>, IHasContextMenu
|
||||
{
|
||||
/// <summary>
|
||||
/// How much padding around the selection area is added.
|
||||
/// </summary>
|
||||
public const float INFLATE_SIZE = 5;
|
||||
|
||||
/// <summary>
|
||||
/// The currently selected blueprints.
|
||||
/// Should be used when operations are dealing directly with the visible blueprints.
|
||||
@ -346,7 +351,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
for (int i = 1; i < selectedBlueprints.Count; i++)
|
||||
selectionRect = RectangleF.Union(selectionRect, ToLocalSpace(selectedBlueprints[i].SelectionQuad).AABBFloat);
|
||||
|
||||
selectionRect = selectionRect.Inflate(5f);
|
||||
selectionRect = selectionRect.Inflate(INFLATE_SIZE);
|
||||
|
||||
SelectionBox.Position = selectionRect.Location;
|
||||
SelectionBox.Size = selectionRect.Size;
|
||||
|
@ -185,6 +185,7 @@ namespace osu.Game.Screens.Edit
|
||||
|
||||
private Bindable<float> editorBackgroundDim;
|
||||
private Bindable<bool> editorHitMarkers;
|
||||
private Bindable<bool> editorAutoSeekOnPlacement;
|
||||
|
||||
public Editor(EditorLoader loader = null)
|
||||
{
|
||||
@ -272,6 +273,7 @@ namespace osu.Game.Screens.Edit
|
||||
|
||||
editorBackgroundDim = config.GetBindable<float>(OsuSetting.EditorDim);
|
||||
editorHitMarkers = config.GetBindable<bool>(OsuSetting.EditorShowHitMarkers);
|
||||
editorAutoSeekOnPlacement = config.GetBindable<bool>(OsuSetting.EditorAutoSeekOnPlacement);
|
||||
|
||||
AddInternal(new OsuContextMenuContainer
|
||||
{
|
||||
@ -329,6 +331,10 @@ namespace osu.Game.Screens.Edit
|
||||
new ToggleMenuItem(EditorStrings.ShowHitMarkers)
|
||||
{
|
||||
State = { BindTarget = editorHitMarkers },
|
||||
},
|
||||
new ToggleMenuItem(EditorStrings.AutoSeekOnPlacement)
|
||||
{
|
||||
State = { BindTarget = editorAutoSeekOnPlacement },
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -16,7 +16,7 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
public partial class BPMCounter : RollingCounter<double>, ISkinnableDrawable
|
||||
public partial class BPMCounter : RollingCounter<double>, ISerialisableDrawable
|
||||
{
|
||||
protected override double RollingDuration => 750;
|
||||
|
||||
|
@ -14,7 +14,7 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD.ClicksPerSecond
|
||||
{
|
||||
public partial class ClicksPerSecondCounter : RollingCounter<int>, ISkinnableDrawable
|
||||
public partial class ClicksPerSecondCounter : RollingCounter<int>, ISerialisableDrawable
|
||||
{
|
||||
[Resolved]
|
||||
private ClicksPerSecondCalculator calculator { get; set; } = null!;
|
||||
|
@ -7,7 +7,7 @@ using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
public abstract partial class ComboCounter : RollingCounter<int>, ISkinnableDrawable
|
||||
public abstract partial class ComboCounter : RollingCounter<int>, ISerialisableDrawable
|
||||
{
|
||||
public bool UsesFixedAnchor { get; set; }
|
||||
|
||||
|
@ -9,7 +9,7 @@ using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
public partial class DefaultAccuracyCounter : GameplayAccuracyCounter, ISkinnableDrawable
|
||||
public partial class DefaultAccuracyCounter : GameplayAccuracyCounter, ISerialisableDrawable
|
||||
{
|
||||
public bool UsesFixedAnchor { get; set; }
|
||||
|
||||
|
@ -19,7 +19,7 @@ using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
public partial class DefaultHealthDisplay : HealthDisplay, IHasAccentColour, ISkinnableDrawable
|
||||
public partial class DefaultHealthDisplay : HealthDisplay, IHasAccentColour, ISerialisableDrawable
|
||||
{
|
||||
/// <summary>
|
||||
/// The base opacity of the glow.
|
||||
|
@ -10,7 +10,7 @@ using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
public partial class DefaultScoreCounter : GameplayScoreCounter, ISkinnableDrawable
|
||||
public partial class DefaultScoreCounter : GameplayScoreCounter, ISerialisableDrawable
|
||||
{
|
||||
public DefaultScoreCounter()
|
||||
{
|
||||
|
@ -14,7 +14,7 @@ using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
||||
{
|
||||
public abstract partial class HitErrorMeter : CompositeDrawable, ISkinnableDrawable
|
||||
public abstract partial class HitErrorMeter : CompositeDrawable, ISerialisableDrawable
|
||||
{
|
||||
protected HitWindows HitWindows { get; private set; }
|
||||
|
||||
|
@ -15,7 +15,7 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD.JudgementCounter
|
||||
{
|
||||
public partial class JudgementCounterDisplay : CompositeDrawable, ISkinnableDrawable
|
||||
public partial class JudgementCounterDisplay : CompositeDrawable, ISerialisableDrawable
|
||||
{
|
||||
public const int TRANSFORM_DURATION = 250;
|
||||
|
||||
|
@ -35,7 +35,7 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
public partial class PerformancePointsCounter : RollingCounter<int>, ISkinnableDrawable
|
||||
public partial class PerformancePointsCounter : RollingCounter<int>, ISerialisableDrawable
|
||||
{
|
||||
public bool UsesFixedAnchor { get; set; }
|
||||
|
||||
|
@ -14,7 +14,7 @@ using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
public abstract partial class SongProgress : OverlayContainer, ISkinnableDrawable
|
||||
public abstract partial class SongProgress : OverlayContainer, ISerialisableDrawable
|
||||
{
|
||||
// Some implementations of this element allow seeking during gameplay playback.
|
||||
// Set a sane default of never handling input to override the behaviour provided by OverlayContainer.
|
||||
|
@ -20,7 +20,7 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
public partial class UnstableRateCounter : RollingCounter<int>, ISkinnableDrawable
|
||||
public partial class UnstableRateCounter : RollingCounter<int>, ISerialisableDrawable
|
||||
{
|
||||
public bool UsesFixedAnchor { get; set; }
|
||||
|
||||
|
@ -88,7 +88,7 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
private readonly BindableBool holdingForHUD = new BindableBool();
|
||||
|
||||
private readonly SkinnableTargetContainer mainComponents;
|
||||
private readonly SkinComponentsContainer mainComponents;
|
||||
|
||||
/// <summary>
|
||||
/// A flow which sits at the left side of the screen to house leaderboard (and related) components.
|
||||
@ -390,7 +390,7 @@ namespace osu.Game.Screens.Play
|
||||
}
|
||||
}
|
||||
|
||||
private partial class MainComponentsContainer : SkinnableTargetContainer
|
||||
private partial class MainComponentsContainer : SkinComponentsContainer
|
||||
{
|
||||
private Bindable<ScoringMode> scoringMode;
|
||||
|
||||
@ -398,7 +398,7 @@ namespace osu.Game.Screens.Play
|
||||
private OsuConfigManager config { get; set; }
|
||||
|
||||
public MainComponentsContainer()
|
||||
: base(GlobalSkinComponentLookup.LookupType.MainHUDComponents)
|
||||
: base(new SkinComponentsContainerLookup(SkinComponentsContainerLookup.TargetArea.MainHUDComponents))
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
@ -417,7 +417,7 @@ namespace osu.Game.Screens.Play
|
||||
lowPassFilter.CutoffTo(1000, 650, Easing.OutQuint);
|
||||
highPassFilter.CutoffTo(300).Then().CutoffTo(0, 1250); // 1250 is to line up with the appearance of MetadataInfo (750 delay + 500 fade-in)
|
||||
|
||||
ApplyToBackground(b => b?.FadeColour(Color4.White, 800, Easing.OutQuint));
|
||||
ApplyToBackground(b => b.FadeColour(Color4.White, 800, Easing.OutQuint));
|
||||
}
|
||||
|
||||
protected virtual void ContentOut()
|
||||
|
@ -1,9 +1,9 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Screens.Backgrounds;
|
||||
|
||||
namespace osu.Game.Screens.Play
|
||||
@ -12,6 +12,11 @@ namespace osu.Game.Screens.Play
|
||||
{
|
||||
protected override BackgroundScreen CreateBackground() => new BackgroundScreenBeatmap(Beatmap.Value);
|
||||
|
||||
public void ApplyToBackground(Action<BackgroundScreenBeatmap> action) => base.ApplyToBackground(b => action.Invoke((BackgroundScreenBeatmap)b));
|
||||
public void ApplyToBackground(Action<BackgroundScreenBeatmap> action)
|
||||
{
|
||||
Debug.Assert(this.IsCurrentScreen());
|
||||
|
||||
base.ApplyToBackground(b => action.Invoke((BackgroundScreenBeatmap)b));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Mods;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Screens.Backgrounds;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Menu;
|
||||
using osu.Game.Screens.Play;
|
||||
@ -148,7 +149,7 @@ namespace osu.Game.Screens.Select
|
||||
if (!this.IsCurrentScreen())
|
||||
return;
|
||||
|
||||
ApplyToBackground(b => b.BlurAmount.Value = e.NewValue ? BACKGROUND_BLUR : 0);
|
||||
ApplyToBackground(applyBlurToBackground);
|
||||
});
|
||||
|
||||
LoadComponentAsync(Carousel = new BeatmapCarousel
|
||||
@ -194,6 +195,7 @@ namespace osu.Game.Screens.Select
|
||||
{
|
||||
ParallaxAmount = 0.005f,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Child = new WedgeBackground
|
||||
@ -273,7 +275,7 @@ namespace osu.Game.Screens.Select
|
||||
}
|
||||
}
|
||||
},
|
||||
new SkinnableTargetContainer(GlobalSkinComponentLookup.LookupType.SongSelect)
|
||||
new SkinComponentsContainer(new SkinComponentsContainerLookup(SkinComponentsContainerLookup.TargetArea.SongSelect))
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
@ -762,13 +764,19 @@ namespace osu.Game.Screens.Select
|
||||
/// </summary>
|
||||
/// <param name="beatmap">The working beatmap.</param>
|
||||
private void updateComponentFromBeatmap(WorkingBeatmap beatmap)
|
||||
{
|
||||
// If not the current screen, this will be applied in OnResuming.
|
||||
if (this.IsCurrentScreen())
|
||||
{
|
||||
ApplyToBackground(backgroundModeBeatmap =>
|
||||
{
|
||||
backgroundModeBeatmap.Beatmap = beatmap;
|
||||
backgroundModeBeatmap.BlurAmount.Value = configBackgroundBlur.Value ? BACKGROUND_BLUR : 0f;
|
||||
backgroundModeBeatmap.IgnoreUserSettings.Value = true;
|
||||
backgroundModeBeatmap.FadeColour(Color4.White, 250);
|
||||
|
||||
applyBlurToBackground(backgroundModeBeatmap);
|
||||
});
|
||||
}
|
||||
|
||||
beatmapInfoWedge.Beatmap = beatmap;
|
||||
|
||||
@ -785,6 +793,14 @@ namespace osu.Game.Screens.Select
|
||||
}
|
||||
}
|
||||
|
||||
private void applyBlurToBackground(BackgroundScreenBeatmap backgroundModeBeatmap)
|
||||
{
|
||||
backgroundModeBeatmap.BlurAmount.Value = configBackgroundBlur.Value ? BACKGROUND_BLUR : 0f;
|
||||
backgroundModeBeatmap.DimWhenUserSettingsIgnored.Value = configBackgroundBlur.Value ? 0 : 0.4f;
|
||||
|
||||
wedgeBackground.FadeTo(configBackgroundBlur.Value ? 0.5f : 0.2f, UserDimContainer.BACKGROUND_FADE_DURATION, Easing.OutQuint);
|
||||
}
|
||||
|
||||
private readonly WeakReference<ITrack?> lastTrack = new WeakReference<ITrack?>(null);
|
||||
|
||||
/// <summary>
|
||||
|
@ -1,14 +1,11 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
|
||||
namespace osu.Game.Screens.Select
|
||||
{
|
||||
@ -22,7 +19,7 @@ namespace osu.Game.Screens.Select
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Size = new Vector2(1, 0.5f),
|
||||
Colour = Color4.Black.Opacity(0.5f),
|
||||
Colour = Color4.Black,
|
||||
Shear = new Vector2(0.15f, 0),
|
||||
EdgeSmoothness = new Vector2(2, 0),
|
||||
},
|
||||
@ -32,7 +29,7 @@ namespace osu.Game.Screens.Select
|
||||
RelativePositionAxes = Axes.Y,
|
||||
Size = new Vector2(1, -0.5f),
|
||||
Position = new Vector2(0, 1),
|
||||
Colour = Color4.Black.Opacity(0.5f),
|
||||
Colour = Color4.Black,
|
||||
Shear = new Vector2(-0.15f, 0),
|
||||
EdgeSmoothness = new Vector2(2, 0),
|
||||
},
|
||||
|
@ -90,19 +90,19 @@ namespace osu.Game.Skinning
|
||||
|
||||
switch (lookup)
|
||||
{
|
||||
case GlobalSkinComponentLookup globalLookup:
|
||||
switch (globalLookup.Lookup)
|
||||
case SkinComponentsContainerLookup containerLookup:
|
||||
switch (containerLookup.Target)
|
||||
{
|
||||
case GlobalSkinComponentLookup.LookupType.SongSelect:
|
||||
var songSelectComponents = new SkinnableTargetComponentsContainer(_ =>
|
||||
case SkinComponentsContainerLookup.TargetArea.SongSelect:
|
||||
var songSelectComponents = new DefaultSkinComponentsContainer(_ =>
|
||||
{
|
||||
// do stuff when we need to.
|
||||
});
|
||||
|
||||
return songSelectComponents;
|
||||
|
||||
case GlobalSkinComponentLookup.LookupType.MainHUDComponents:
|
||||
var skinnableTargetWrapper = new SkinnableTargetComponentsContainer(container =>
|
||||
case SkinComponentsContainerLookup.TargetArea.MainHUDComponents:
|
||||
var skinnableTargetWrapper = new DefaultSkinComponentsContainer(container =>
|
||||
{
|
||||
var score = container.OfType<DefaultScoreCounter>().FirstOrDefault();
|
||||
var accuracy = container.OfType<DefaultAccuracyCounter>().FirstOrDefault();
|
||||
|
@ -19,7 +19,7 @@ namespace osu.Game.Skinning.Components
|
||||
/// Intended to be a test bed for skinning. May be removed at some point in the future.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public partial class BigBlackBox : CompositeDrawable, ISkinnableDrawable
|
||||
public partial class BigBlackBox : CompositeDrawable, ISerialisableDrawable
|
||||
{
|
||||
public bool UsesFixedAnchor { get; set; }
|
||||
|
||||
|
@ -2,39 +2,28 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
/// <summary>
|
||||
/// A container which groups the components of a <see cref="SkinnableTargetContainer"/> into a single object.
|
||||
/// Optionally also applies a default layout to the components.
|
||||
/// A container which can be used to specify default skin components layouts.
|
||||
/// Handles applying a default layout to the components.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public partial class SkinnableTargetComponentsContainer : Container, ISkinnableDrawable
|
||||
public partial class DefaultSkinComponentsContainer : Container
|
||||
{
|
||||
public bool IsEditable => false;
|
||||
|
||||
public bool UsesFixedAnchor { get; set; }
|
||||
|
||||
private readonly Action<Container>? applyDefaults;
|
||||
|
||||
/// <summary>
|
||||
/// Construct a wrapper with defaults that should be applied once.
|
||||
/// </summary>
|
||||
/// <param name="applyDefaults">A function to apply the default layout.</param>
|
||||
public SkinnableTargetComponentsContainer(Action<Container> applyDefaults)
|
||||
: this()
|
||||
{
|
||||
this.applyDefaults = applyDefaults;
|
||||
}
|
||||
|
||||
[JsonConstructor]
|
||||
public SkinnableTargetComponentsContainer()
|
||||
public DefaultSkinComponentsContainer(Action<Container> applyDefaults)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
this.applyDefaults = applyDefaults;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
@ -13,7 +13,7 @@ namespace osu.Game.Skinning
|
||||
/// <summary>
|
||||
/// A skin component that contains text and allows the user to choose its font.
|
||||
/// </summary>
|
||||
public abstract partial class FontAdjustableSkinComponent : Container, ISkinnableDrawable
|
||||
public abstract partial class FontAdjustableSkinComponent : Container, ISerialisableDrawable
|
||||
{
|
||||
public bool UsesFixedAnchor { get; set; }
|
||||
|
||||
|
@ -1,12 +1,23 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
/// <summary>
|
||||
/// A lookup type intended for use for skinnable gameplay components (not HUD level components).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The most common usage of this class is for ruleset-specific skinning implementations, but it can also be used directly
|
||||
/// (see <see cref="DrawableJudgement"/>'s usage for <see cref="HitResult"/>) where ruleset-agnostic elements are required.
|
||||
/// </remarks>
|
||||
/// <typeparam name="T">An enum lookup type.</typeparam>
|
||||
public class GameplaySkinComponentLookup<T> : ISkinComponentLookup
|
||||
where T : notnull
|
||||
where T : Enum
|
||||
{
|
||||
public readonly T Component;
|
||||
|
||||
@ -16,7 +27,7 @@ namespace osu.Game.Skinning
|
||||
}
|
||||
|
||||
protected virtual string RulesetPrefix => string.Empty;
|
||||
protected virtual string ComponentName => Component.ToString() ?? string.Empty;
|
||||
protected virtual string ComponentName => Component.ToString();
|
||||
|
||||
public string LookupName =>
|
||||
string.Join('/', new[] { "Gameplay", RulesetPrefix, ComponentName }.Where(s => !string.IsNullOrEmpty(s)));
|
||||
|
@ -1,21 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
public class GlobalSkinComponentLookup : ISkinComponentLookup
|
||||
{
|
||||
public readonly LookupType Lookup;
|
||||
|
||||
public GlobalSkinComponentLookup(LookupType lookup)
|
||||
{
|
||||
Lookup = lookup;
|
||||
}
|
||||
|
||||
public enum LookupType
|
||||
{
|
||||
MainHUDComponents,
|
||||
SongSelect
|
||||
}
|
||||
}
|
||||
}
|
@ -10,13 +10,16 @@ using osu.Game.Configuration;
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
/// <summary>
|
||||
/// Denotes a drawable which, as a drawable, can be adjusted via skinning specifications.
|
||||
/// A drawable which is intended to be serialised to <see cref="SerialisedDrawableInfo"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Attaching this interface to any <see cref="IDrawable"/> will make it serialisable to skin settings.
|
||||
/// Adding <see cref="SettingSourceAttribute"/> annotated bindables will also serialise these settings alongside each instance.
|
||||
/// This is currently used exclusively for serialisation to a skin, and leaned on heavily to allow placement and customisation in the skin layout editor.
|
||||
/// That said, it is intended to be flexible enough to potentially be used in other places we want to serialise drawables in the future.
|
||||
///
|
||||
/// Attaching this interface to any <see cref="IDrawable"/> will make it serialisable via <see cref="SerialisableDrawableExtensions.CreateSerialisedInfo"/>.
|
||||
/// Adding <see cref="SettingSourceAttribute"/> annotated bindables will also allow serialising settings automatically.
|
||||
/// </remarks>
|
||||
public interface ISkinnableDrawable : IDrawable
|
||||
public interface ISerialisableDrawable : IDrawable
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether this component should be editable by an end user.
|
||||
@ -24,8 +27,8 @@ namespace osu.Game.Skinning
|
||||
bool IsEditable => true;
|
||||
|
||||
/// <summary>
|
||||
/// In the context of the skin layout editor, whether this <see cref="ISkinnableDrawable"/> has a permanent anchor defined.
|
||||
/// If <see langword="false"/>, this <see cref="ISkinnableDrawable"/>'s <see cref="Drawable.Anchor"/> is automatically determined by proximity,
|
||||
/// In the context of the skin layout editor, whether this <see cref="ISerialisableDrawable"/> has a permanent anchor defined.
|
||||
/// If <see langword="false"/>, this <see cref="ISerialisableDrawable"/>'s <see cref="Drawable.Anchor"/> is automatically determined by proximity,
|
||||
/// If <see langword="true"/>, a fixed anchor point has been defined.
|
||||
/// </summary>
|
||||
bool UsesFixedAnchor { get; set; }
|
@ -5,31 +5,25 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
/// <summary>
|
||||
/// Denotes a container which can house <see cref="ISkinnableDrawable"/>s.
|
||||
/// A container which can house <see cref="ISerialisableDrawable"/>s.
|
||||
/// Contains functionality for new drawables to be added, removed, and reloaded from provided <see cref="SerialisedDrawableInfo"/>.
|
||||
/// </summary>
|
||||
public interface ISkinnableTarget : IDrawable
|
||||
public interface ISerialisableDrawableContainer : IDrawable
|
||||
{
|
||||
/// <summary>
|
||||
/// The definition of this target.
|
||||
/// </summary>
|
||||
GlobalSkinComponentLookup.LookupType Target { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A bindable list of components which are being tracked by this skinnable target.
|
||||
/// </summary>
|
||||
IBindableList<ISkinnableDrawable> Components { get; }
|
||||
IBindableList<ISerialisableDrawable> Components { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Serialise all children as <see cref="SkinnableInfo"/>.
|
||||
/// Serialise all children as <see cref="SerialisedDrawableInfo"/>.
|
||||
/// </summary>
|
||||
/// <returns>The serialised content.</returns>
|
||||
IEnumerable<SkinnableInfo> CreateSkinnableInfo() => Components.Select(d => ((Drawable)d).CreateSkinnableInfo());
|
||||
IEnumerable<SerialisedDrawableInfo> CreateSerialisedInfo() => Components.Select(d => ((Drawable)d).CreateSerialisedInfo());
|
||||
|
||||
/// <summary>
|
||||
/// Reload this target from the current skin.
|
||||
@ -39,18 +33,18 @@ namespace osu.Game.Skinning
|
||||
/// <summary>
|
||||
/// Reload this target from the provided skinnable information.
|
||||
/// </summary>
|
||||
void Reload(SkinnableInfo[] skinnableInfo);
|
||||
void Reload(SerialisedDrawableInfo[] skinnableInfo);
|
||||
|
||||
/// <summary>
|
||||
/// Add a new skinnable component to this target.
|
||||
/// </summary>
|
||||
/// <param name="drawable">The component to add.</param>
|
||||
void Add(ISkinnableDrawable drawable);
|
||||
void Add(ISerialisableDrawable drawable);
|
||||
|
||||
/// <summary>
|
||||
/// Remove an existing skinnable component from this target.
|
||||
/// </summary>
|
||||
/// <param name="component">The component to remove.</param>
|
||||
void Remove(ISkinnableDrawable component);
|
||||
void Remove(ISerialisableDrawable component);
|
||||
}
|
||||
}
|
@ -10,7 +10,7 @@ using osu.Game.Audio;
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides access to skinnable elements.
|
||||
/// Provides access to various elements contained by a skin.
|
||||
/// </summary>
|
||||
public interface ISkin
|
||||
{
|
||||
|
@ -4,7 +4,8 @@
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
/// <summary>
|
||||
/// A lookup type which can be used with <see cref="ISkin.GetDrawableComponent"/>.
|
||||
/// The base lookup type to be used with <see cref="ISkin.GetDrawableComponent"/>.
|
||||
/// Should be implemented as necessary to add further criteria to lookups, which are usually consumed by ruleset transformers or legacy lookup cases.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Implementations of <see cref="ISkin.GetDrawableComponent"/> should match on types implementing this interface
|
||||
|
@ -7,8 +7,16 @@ using System.Collections.Generic;
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides access to skinnable elements.
|
||||
/// An abstract skin implementation, whose primary purpose is to properly handle component fallback across multiple layers of skins (e.g.: beatmap skin, user skin, default skin).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Common usage is to do an initial lookup via <see cref="FindProvider"/>, and use the returned <see cref="ISkin"/>
|
||||
/// to do further lookups for related components.
|
||||
///
|
||||
/// The initial lookup is used to lock consecutive lookups to the same underlying skin source (as to not get some elements
|
||||
/// from one skin and others from another, which would be the case if using <see cref="ISkin"/> methods like
|
||||
/// <see cref="ISkin.GetSample"/> directly).
|
||||
/// </remarks>
|
||||
public interface ISkinSource : ISkin
|
||||
{
|
||||
/// <summary>
|
||||
|
@ -8,7 +8,7 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
public partial class LegacyAccuracyCounter : GameplayAccuracyCounter, ISkinnableDrawable
|
||||
public partial class LegacyAccuracyCounter : GameplayAccuracyCounter, ISerialisableDrawable
|
||||
{
|
||||
public bool UsesFixedAnchor { get; set; }
|
||||
|
||||
|
@ -45,11 +45,11 @@ namespace osu.Game.Skinning
|
||||
|
||||
public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
|
||||
{
|
||||
if (lookup is GlobalSkinComponentLookup targetComponent)
|
||||
if (lookup is SkinComponentsContainerLookup containerLookup)
|
||||
{
|
||||
switch (targetComponent.Lookup)
|
||||
switch (containerLookup.Target)
|
||||
{
|
||||
case GlobalSkinComponentLookup.LookupType.MainHUDComponents:
|
||||
case SkinComponentsContainerLookup.TargetArea.MainHUDComponents:
|
||||
// this should exist in LegacySkin instead, but there isn't a fallback skin for LegacySkins yet.
|
||||
// therefore keep the check here until fallback default legacy skin is supported.
|
||||
if (!this.HasFont(LegacyFont.Score))
|
||||
|
@ -7,7 +7,6 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Skinning
|
||||
@ -15,7 +14,7 @@ namespace osu.Game.Skinning
|
||||
/// <summary>
|
||||
/// Uses the 'x' symbol and has a pop-out effect while rolling over.
|
||||
/// </summary>
|
||||
public partial class LegacyComboCounter : CompositeDrawable, ISkinnableDrawable
|
||||
public partial class LegacyComboCounter : CompositeDrawable, ISerialisableDrawable
|
||||
{
|
||||
public Bindable<int> Current { get; } = new BindableInt { MinValue = 0 };
|
||||
|
||||
@ -45,7 +44,7 @@ namespace osu.Game.Skinning
|
||||
private readonly Container counterContainer;
|
||||
|
||||
/// <summary>
|
||||
/// Hides the combo counter internally without affecting its <see cref="SkinnableInfo"/>.
|
||||
/// Hides the combo counter internally without affecting its <see cref="SerialisedDrawableInfo"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is used for rulesets that provide their own combo counter and don't want this HUD one to be visible,
|
||||
|
@ -19,7 +19,7 @@ using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
public partial class LegacyHealthDisplay : HealthDisplay, ISkinnableDrawable
|
||||
public partial class LegacyHealthDisplay : HealthDisplay, ISerialisableDrawable
|
||||
{
|
||||
private const double epic_cutoff = 0.5;
|
||||
|
||||
|
@ -8,7 +8,7 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
public partial class LegacyScoreCounter : GameplayScoreCounter, ISkinnableDrawable
|
||||
public partial class LegacyScoreCounter : GameplayScoreCounter, ISerialisableDrawable
|
||||
{
|
||||
protected override double RollingDuration => 1000;
|
||||
protected override Easing RollingEasing => Easing.Out;
|
||||
|
@ -343,11 +343,11 @@ namespace osu.Game.Skinning
|
||||
|
||||
switch (lookup)
|
||||
{
|
||||
case GlobalSkinComponentLookup target:
|
||||
switch (target.Lookup)
|
||||
case SkinComponentsContainerLookup containerLookup:
|
||||
switch (containerLookup.Target)
|
||||
{
|
||||
case GlobalSkinComponentLookup.LookupType.MainHUDComponents:
|
||||
var skinnableTargetWrapper = new SkinnableTargetComponentsContainer(container =>
|
||||
case SkinComponentsContainerLookup.TargetArea.MainHUDComponents:
|
||||
var skinnableTargetWrapper = new DefaultSkinComponentsContainer(container =>
|
||||
{
|
||||
var score = container.OfType<LegacyScoreCounter>().FirstOrDefault();
|
||||
var accuracy = container.OfType<GameplayAccuracyCounter>().FirstOrDefault();
|
||||
|
51
osu.Game/Skinning/SerialisableDrawableExtensions.cs
Normal file
51
osu.Game/Skinning/SerialisableDrawableExtensions.cs
Normal file
@ -0,0 +1,51 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Extensions;
|
||||
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
public static class SerialisableDrawableExtensions
|
||||
{
|
||||
public static SerialisedDrawableInfo CreateSerialisedInfo(this Drawable component) => new SerialisedDrawableInfo(component);
|
||||
|
||||
public static void ApplySerialisedInfo(this Drawable component, SerialisedDrawableInfo drawableInfo)
|
||||
{
|
||||
// todo: can probably make this better via deserialisation directly using a common interface.
|
||||
component.Position = drawableInfo.Position;
|
||||
component.Rotation = drawableInfo.Rotation;
|
||||
component.Scale = drawableInfo.Scale;
|
||||
component.Anchor = drawableInfo.Anchor;
|
||||
component.Origin = drawableInfo.Origin;
|
||||
|
||||
if (component is ISerialisableDrawable serialisableDrawable)
|
||||
{
|
||||
serialisableDrawable.UsesFixedAnchor = drawableInfo.UsesFixedAnchor;
|
||||
|
||||
foreach (var (_, property) in component.GetSettingsSourceProperties())
|
||||
{
|
||||
var bindable = ((IBindable)property.GetValue(component)!);
|
||||
|
||||
if (!drawableInfo.Settings.TryGetValue(property.Name.ToSnakeCase(), out object? settingValue))
|
||||
{
|
||||
// TODO: We probably want to restore default if not included in serialisation information.
|
||||
// This is not simple to do as SetDefault() is only found in the typed Bindable<T> interface right now.
|
||||
continue;
|
||||
}
|
||||
|
||||
serialisableDrawable.CopyAdjustedSetting(bindable, settingValue);
|
||||
}
|
||||
}
|
||||
|
||||
if (component is Container container)
|
||||
{
|
||||
foreach (var child in drawableInfo.Children)
|
||||
container.Add(child.CreateInstance());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@ -13,18 +11,22 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
/// <summary>
|
||||
/// Serialised information governing custom changes to an <see cref="ISkinnableDrawable"/>.
|
||||
/// Serialised backing data for <see cref="ISerialisableDrawable"/>s.
|
||||
/// Used for json serialisation in user skins.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Can be created using <see cref="SerialisableDrawableExtensions.CreateSerialisedInfo"/>.
|
||||
/// Can also be applied to an existing drawable using <see cref="SerialisableDrawableExtensions.ApplySerialisedInfo"/>.
|
||||
/// </remarks>
|
||||
[Serializable]
|
||||
public class SkinnableInfo
|
||||
public sealed class SerialisedDrawableInfo
|
||||
{
|
||||
public Type Type { get; set; }
|
||||
public Type Type { get; set; } = null!;
|
||||
|
||||
public Vector2 Position { get; set; }
|
||||
|
||||
@ -36,15 +38,15 @@ namespace osu.Game.Screens.Play.HUD
|
||||
|
||||
public Anchor Origin { get; set; }
|
||||
|
||||
/// <inheritdoc cref="ISkinnableDrawable.UsesFixedAnchor"/>
|
||||
/// <inheritdoc cref="ISerialisableDrawable.UsesFixedAnchor"/>
|
||||
public bool UsesFixedAnchor { get; set; }
|
||||
|
||||
public Dictionary<string, object> Settings { get; set; } = new Dictionary<string, object>();
|
||||
|
||||
public List<SkinnableInfo> Children { get; } = new List<SkinnableInfo>();
|
||||
public List<SerialisedDrawableInfo> Children { get; } = new List<SerialisedDrawableInfo>();
|
||||
|
||||
[JsonConstructor]
|
||||
public SkinnableInfo()
|
||||
public SerialisedDrawableInfo()
|
||||
{
|
||||
}
|
||||
|
||||
@ -52,7 +54,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
/// Construct a new instance populating all attributes from the provided drawable.
|
||||
/// </summary>
|
||||
/// <param name="component">The drawable which attributes should be sourced from.</param>
|
||||
public SkinnableInfo(Drawable component)
|
||||
public SerialisedDrawableInfo(Drawable component)
|
||||
{
|
||||
Type = component.GetType();
|
||||
|
||||
@ -62,8 +64,8 @@ namespace osu.Game.Screens.Play.HUD
|
||||
Anchor = component.Anchor;
|
||||
Origin = component.Origin;
|
||||
|
||||
if (component is ISkinnableDrawable skinnable)
|
||||
UsesFixedAnchor = skinnable.UsesFixedAnchor;
|
||||
if (component is ISerialisableDrawable serialisableDrawable)
|
||||
UsesFixedAnchor = serialisableDrawable.UsesFixedAnchor;
|
||||
|
||||
foreach (var (_, property) in component.GetSettingsSourceProperties())
|
||||
{
|
||||
@ -74,8 +76,8 @@ namespace osu.Game.Screens.Play.HUD
|
||||
|
||||
if (component is Container<Drawable> container)
|
||||
{
|
||||
foreach (var child in container.OfType<ISkinnableDrawable>().OfType<Drawable>())
|
||||
Children.Add(child.CreateSkinnableInfo());
|
||||
foreach (var child in container.OfType<ISerialisableDrawable>().OfType<Drawable>())
|
||||
Children.Add(child.CreateSerialisedInfo());
|
||||
}
|
||||
}
|
||||
|
||||
@ -88,7 +90,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
try
|
||||
{
|
||||
Drawable d = (Drawable)Activator.CreateInstance(Type)!;
|
||||
d.ApplySkinnableInfo(this);
|
||||
d.ApplySerialisedInfo(this);
|
||||
return d;
|
||||
}
|
||||
catch (Exception e)
|
||||
@ -102,7 +104,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
return typeof(OsuGame).Assembly.GetTypes()
|
||||
.Where(t => !t.IsInterface && !t.IsAbstract)
|
||||
.Where(t => typeof(ISkinnableDrawable).IsAssignableFrom(t))
|
||||
.Where(t => typeof(ISerialisableDrawable).IsAssignableFrom(t))
|
||||
.OrderBy(t => t.Name)
|
||||
.ToArray();
|
||||
}
|
@ -11,13 +11,13 @@ using Newtonsoft.Json;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.IO.Stores;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
@ -37,9 +37,9 @@ namespace osu.Game.Skinning
|
||||
|
||||
public SkinConfiguration Configuration { get; set; }
|
||||
|
||||
public IDictionary<GlobalSkinComponentLookup.LookupType, SkinnableInfo[]> DrawableComponentInfo => drawableComponentInfo;
|
||||
public IDictionary<SkinComponentsContainerLookup.TargetArea, SerialisedDrawableInfo[]> DrawableComponentInfo => drawableComponentInfo;
|
||||
|
||||
private readonly Dictionary<GlobalSkinComponentLookup.LookupType, SkinnableInfo[]> drawableComponentInfo = new Dictionary<GlobalSkinComponentLookup.LookupType, SkinnableInfo[]>();
|
||||
private readonly Dictionary<SkinComponentsContainerLookup.TargetArea, SerialisedDrawableInfo[]> drawableComponentInfo = new Dictionary<SkinComponentsContainerLookup.TargetArea, SerialisedDrawableInfo[]>();
|
||||
|
||||
public abstract ISample? GetSample(ISampleInfo sampleInfo);
|
||||
|
||||
@ -100,7 +100,7 @@ namespace osu.Game.Skinning
|
||||
Configuration = new SkinConfiguration();
|
||||
|
||||
// skininfo files may be null for default skin.
|
||||
foreach (GlobalSkinComponentLookup.LookupType skinnableTarget in Enum.GetValues<GlobalSkinComponentLookup.LookupType>())
|
||||
foreach (SkinComponentsContainerLookup.TargetArea skinnableTarget in Enum.GetValues<SkinComponentsContainerLookup.TargetArea>())
|
||||
{
|
||||
string filename = $"{skinnableTarget}.json";
|
||||
|
||||
@ -119,7 +119,7 @@ namespace osu.Game.Skinning
|
||||
jsonContent = jsonContent.Replace(@"osu.Game.Screens.Play.SongProgress", @"osu.Game.Screens.Play.HUD.DefaultSongProgress");
|
||||
jsonContent = jsonContent.Replace(@"osu.Game.Screens.Play.HUD.LegacyComboCounter", @"osu.Game.Skinning.LegacyComboCounter");
|
||||
|
||||
var deserializedContent = JsonConvert.DeserializeObject<IEnumerable<SkinnableInfo>>(jsonContent);
|
||||
var deserializedContent = JsonConvert.DeserializeObject<IEnumerable<SerialisedDrawableInfo>>(jsonContent);
|
||||
|
||||
if (deserializedContent == null)
|
||||
continue;
|
||||
@ -143,18 +143,18 @@ namespace osu.Game.Skinning
|
||||
/// Remove all stored customisations for the provided target.
|
||||
/// </summary>
|
||||
/// <param name="targetContainer">The target container to reset.</param>
|
||||
public void ResetDrawableTarget(ISkinnableTarget targetContainer)
|
||||
public void ResetDrawableTarget(SkinComponentsContainer targetContainer)
|
||||
{
|
||||
DrawableComponentInfo.Remove(targetContainer.Target);
|
||||
DrawableComponentInfo.Remove(targetContainer.Lookup.Target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update serialised information for the provided target.
|
||||
/// </summary>
|
||||
/// <param name="targetContainer">The target container to serialise to this skin.</param>
|
||||
public void UpdateDrawableTarget(ISkinnableTarget targetContainer)
|
||||
public void UpdateDrawableTarget(SkinComponentsContainer targetContainer)
|
||||
{
|
||||
DrawableComponentInfo[targetContainer.Target] = targetContainer.CreateSkinnableInfo().ToArray();
|
||||
DrawableComponentInfo[targetContainer.Lookup.Target] = ((ISerialisableDrawableContainer)targetContainer).CreateSerialisedInfo().ToArray();
|
||||
}
|
||||
|
||||
public virtual Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
|
||||
@ -165,8 +165,8 @@ namespace osu.Game.Skinning
|
||||
case SkinnableSprite.SpriteComponentLookup sprite:
|
||||
return this.GetAnimation(sprite.LookupName, false, false);
|
||||
|
||||
case GlobalSkinComponentLookup target:
|
||||
if (!DrawableComponentInfo.TryGetValue(target.Lookup, out var skinnableInfo))
|
||||
case SkinComponentsContainerLookup containerLookup:
|
||||
if (!DrawableComponentInfo.TryGetValue(containerLookup.Target, out var skinnableInfo))
|
||||
return null;
|
||||
|
||||
var components = new List<Drawable>();
|
||||
@ -174,8 +174,9 @@ namespace osu.Game.Skinning
|
||||
foreach (var i in skinnableInfo)
|
||||
components.Add(i.CreateInstance());
|
||||
|
||||
return new SkinnableTargetComponentsContainer
|
||||
return new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = components,
|
||||
};
|
||||
}
|
||||
|
@ -7,19 +7,31 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
public partial class SkinnableTargetContainer : SkinReloadableDrawable, ISkinnableTarget
|
||||
/// <summary>
|
||||
/// A container which holds many skinnable components, with functionality to add, remove and reload layouts.
|
||||
/// Used to allow user customisation of skin layouts.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is currently used as a means of serialising skin layouts to files.
|
||||
/// Currently, one json file in a skin will represent one <see cref="SkinComponentsContainer"/>, containing
|
||||
/// the output of <see cref="ISerialisableDrawableContainer.CreateSerialisedInfo"/>.
|
||||
/// </remarks>
|
||||
public partial class SkinComponentsContainer : SkinReloadableDrawable, ISerialisableDrawableContainer
|
||||
{
|
||||
private SkinnableTargetComponentsContainer? content;
|
||||
private Container? content;
|
||||
|
||||
public GlobalSkinComponentLookup.LookupType Target { get; }
|
||||
/// <summary>
|
||||
/// The lookup criteria which will be used to retrieve components from the active skin.
|
||||
/// </summary>
|
||||
public SkinComponentsContainerLookup Lookup { get; }
|
||||
|
||||
public IBindableList<ISkinnableDrawable> Components => components;
|
||||
public IBindableList<ISerialisableDrawable> Components => components;
|
||||
|
||||
private readonly BindableList<ISkinnableDrawable> components = new BindableList<ISkinnableDrawable>();
|
||||
private readonly BindableList<ISerialisableDrawable> components = new BindableList<ISerialisableDrawable>();
|
||||
|
||||
public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks; // ensure that components are loaded even if the target container is hidden (ie. due to user toggle).
|
||||
|
||||
@ -27,27 +39,28 @@ namespace osu.Game.Skinning
|
||||
|
||||
private CancellationTokenSource? cancellationSource;
|
||||
|
||||
public SkinnableTargetContainer(GlobalSkinComponentLookup.LookupType target)
|
||||
public SkinComponentsContainer(SkinComponentsContainerLookup lookup)
|
||||
{
|
||||
Target = target;
|
||||
Lookup = lookup;
|
||||
}
|
||||
|
||||
public void Reload(SkinnableInfo[] skinnableInfo)
|
||||
public void Reload(SerialisedDrawableInfo[] skinnableInfo)
|
||||
{
|
||||
var drawables = new List<Drawable>();
|
||||
|
||||
foreach (var i in skinnableInfo)
|
||||
drawables.Add(i.CreateInstance());
|
||||
|
||||
Reload(new SkinnableTargetComponentsContainer
|
||||
Reload(new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = drawables,
|
||||
});
|
||||
}
|
||||
|
||||
public void Reload() => Reload(CurrentSkin.GetDrawableComponent(new GlobalSkinComponentLookup(Target)) as SkinnableTargetComponentsContainer);
|
||||
public void Reload() => Reload(CurrentSkin.GetDrawableComponent(Lookup) as Container);
|
||||
|
||||
public void Reload(SkinnableTargetComponentsContainer? componentsContainer)
|
||||
public void Reload(Container? componentsContainer)
|
||||
{
|
||||
ClearInternal();
|
||||
components.Clear();
|
||||
@ -66,7 +79,7 @@ namespace osu.Game.Skinning
|
||||
LoadComponentAsync(content, wrapper =>
|
||||
{
|
||||
AddInternal(wrapper);
|
||||
components.AddRange(wrapper.Children.OfType<ISkinnableDrawable>());
|
||||
components.AddRange(wrapper.Children.OfType<ISerialisableDrawable>());
|
||||
ComponentsLoaded = true;
|
||||
}, (cancellationSource = new CancellationTokenSource()).Token);
|
||||
}
|
||||
@ -74,10 +87,10 @@ namespace osu.Game.Skinning
|
||||
ComponentsLoaded = true;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ISkinnableTarget"/>
|
||||
/// <inheritdoc cref="ISerialisableDrawableContainer"/>
|
||||
/// <exception cref="NotSupportedException">Thrown when attempting to add an element to a target which is not supported by the current skin.</exception>
|
||||
/// <exception cref="ArgumentException">Thrown if the provided instance is not a <see cref="Drawable"/>.</exception>
|
||||
public void Add(ISkinnableDrawable component)
|
||||
public void Add(ISerialisableDrawable component)
|
||||
{
|
||||
if (content == null)
|
||||
throw new NotSupportedException("Attempting to add a new component to a target container which is not supported by the current skin.");
|
||||
@ -89,10 +102,10 @@ namespace osu.Game.Skinning
|
||||
components.Add(component);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ISkinnableTarget"/>
|
||||
/// <inheritdoc cref="ISerialisableDrawableContainer"/>
|
||||
/// <exception cref="NotSupportedException">Thrown when attempting to add an element to a target which is not supported by the current skin.</exception>
|
||||
/// <exception cref="ArgumentException">Thrown if the provided instance is not a <see cref="Drawable"/>.</exception>
|
||||
public void Remove(ISkinnableDrawable component)
|
||||
public void Remove(ISerialisableDrawable component)
|
||||
{
|
||||
if (content == null)
|
||||
throw new NotSupportedException("Attempting to remove a new component from a target container which is not supported by the current skin.");
|
30
osu.Game/Skinning/SkinComponentsContainerLookup.cs
Normal file
30
osu.Game/Skinning/SkinComponentsContainerLookup.cs
Normal file
@ -0,0 +1,30 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a lookup of a collection of elements that make up a particular skinnable <see cref="TargetArea"/> of the game.
|
||||
/// </summary>
|
||||
public class SkinComponentsContainerLookup : ISkinComponentLookup
|
||||
{
|
||||
/// <summary>
|
||||
/// The target area / layer of the game for which skin components will be returned.
|
||||
/// </summary>
|
||||
public readonly TargetArea Target;
|
||||
|
||||
public SkinComponentsContainerLookup(TargetArea target)
|
||||
{
|
||||
Target = target;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a particular area or part of a game screen whose layout can be customised using the skin editor.
|
||||
/// </summary>
|
||||
public enum TargetArea
|
||||
{
|
||||
MainHUDComponents,
|
||||
SongSelect
|
||||
}
|
||||
}
|
||||
}
|
@ -15,8 +15,13 @@ using osu.Game.Audio;
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
/// <summary>
|
||||
/// A container which adds a local <see cref="ISkinSource"/> to the hierarchy.
|
||||
/// A container which adds a provided <see cref="ISkin"/> to the DI skin lookup hierarchy.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This container will expose an <see cref="ISkinSource"/> to its children.
|
||||
/// The source will first consider the skin provided via the constructor (if any), then fallback
|
||||
/// to any <see cref="ISkinSource"/> providers in the parent DI hierarchy.
|
||||
/// </remarks>
|
||||
public partial class SkinProvidingContainer : Container, ISkinSource
|
||||
{
|
||||
public event Action? SourceChanged;
|
||||
|
@ -9,7 +9,8 @@ using osu.Framework.Graphics.Pooling;
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
/// <summary>
|
||||
/// A drawable which has a callback when the skin changes.
|
||||
/// A poolable drawable implementation which has a pre-wired callback (see <see cref="SkinChanged"/>) that fires
|
||||
/// once on load and again on any subsequent skin change.
|
||||
/// </summary>
|
||||
public abstract partial class SkinReloadableDrawable : PoolableDrawable
|
||||
{
|
||||
|
@ -10,6 +10,13 @@ using osu.Game.Audio;
|
||||
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
/// <summary>
|
||||
/// A default skin transformer, which falls back to the provided skin by default.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Implementations of skin transformers should generally derive this class and override
|
||||
/// individual lookup methods, modifying the lookup flow as required.
|
||||
/// </remarks>
|
||||
public abstract class SkinTransformer : ISkinTransformer
|
||||
{
|
||||
public ISkin Skin { get; }
|
||||
|
@ -21,7 +21,7 @@ namespace osu.Game.Skinning
|
||||
/// <summary>
|
||||
/// A skinnable element which uses a single texture backing.
|
||||
/// </summary>
|
||||
public partial class SkinnableSprite : SkinnableDrawable, ISkinnableDrawable
|
||||
public partial class SkinnableSprite : SkinnableDrawable, ISerialisableDrawable
|
||||
{
|
||||
protected override bool ApplySizeRestrictionsToDefault => true;
|
||||
|
||||
|
@ -68,19 +68,19 @@ namespace osu.Game.Skinning
|
||||
|
||||
switch (lookup)
|
||||
{
|
||||
case GlobalSkinComponentLookup target:
|
||||
switch (target.Lookup)
|
||||
case SkinComponentsContainerLookup containerLookup:
|
||||
switch (containerLookup.Target)
|
||||
{
|
||||
case GlobalSkinComponentLookup.LookupType.SongSelect:
|
||||
var songSelectComponents = new SkinnableTargetComponentsContainer(_ =>
|
||||
case SkinComponentsContainerLookup.TargetArea.SongSelect:
|
||||
var songSelectComponents = new DefaultSkinComponentsContainer(_ =>
|
||||
{
|
||||
// do stuff when we need to.
|
||||
});
|
||||
|
||||
return songSelectComponents;
|
||||
|
||||
case GlobalSkinComponentLookup.LookupType.MainHUDComponents:
|
||||
var skinnableTargetWrapper = new SkinnableTargetComponentsContainer(container =>
|
||||
case SkinComponentsContainerLookup.TargetArea.MainHUDComponents:
|
||||
var skinnableTargetWrapper = new DefaultSkinComponentsContainer(container =>
|
||||
{
|
||||
var score = container.OfType<DefaultScoreCounter>().FirstOrDefault();
|
||||
var accuracy = container.OfType<DefaultAccuracyCounter>().FirstOrDefault();
|
||||
|
@ -45,13 +45,13 @@ namespace osu.Game.Tests.Visual
|
||||
|
||||
private void addResetTargetsStep()
|
||||
{
|
||||
AddStep("reset targets", () => this.ChildrenOfType<SkinnableTargetContainer>().ForEach(t =>
|
||||
AddStep("reset targets", () => this.ChildrenOfType<SkinComponentsContainer>().ForEach(t =>
|
||||
{
|
||||
LegacySkin.ResetDrawableTarget(t);
|
||||
t.Reload();
|
||||
}));
|
||||
|
||||
AddUntilStep("wait for components to load", () => this.ChildrenOfType<SkinnableTargetContainer>().All(t => t.ComponentsLoaded));
|
||||
AddUntilStep("wait for components to load", () => this.ChildrenOfType<SkinComponentsContainer>().All(t => t.ComponentsLoaded));
|
||||
}
|
||||
|
||||
public partial class SkinProvidingPlayer : TestPlayer
|
||||
|
@ -66,7 +66,7 @@ namespace osu.Game.Tests.Visual
|
||||
protected override bool CheckModsAllowFailure() => allowFail;
|
||||
|
||||
public ModTestPlayer(ModTestData data, bool allowFail)
|
||||
: base(false, false)
|
||||
: base(true, false)
|
||||
{
|
||||
this.allowFail = allowFail;
|
||||
currentTestData = data;
|
||||
|
Loading…
Reference in New Issue
Block a user