mirror of
https://github.com/ppy/osu.git
synced 2025-01-13 05:53:10 +08:00
Merge branch 'master' into fix-realm-state-change-crashes
This commit is contained in:
commit
c85edd2b55
@ -1,14 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||
<data-source source="LOCAL" name="osu-client-sqlite" uuid="1aa4b9be-cd8d-47ae-8186-30a13cd724a5">
|
||||
<driver-ref>sqlite.xerial</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
||||
<jdbc-url>jdbc:sqlite:$USER_HOME$/.local/share/osu/client.db</jdbc-url>
|
||||
<driver-properties>
|
||||
<property name="enable_load_extension" value="true" />
|
||||
</driver-properties>
|
||||
</data-source>
|
||||
</component>
|
||||
</project>
|
14
osu.Game.Rulesets.Catch.Tests/Editor/TestSceneEditor.cs
Normal file
14
osu.Game.Rulesets.Catch.Tests/Editor/TestSceneEditor.cs
Normal file
@ -0,0 +1,14 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestSceneEditor : EditorTestScene
|
||||
{
|
||||
protected override Ruleset CreateEditorRuleset() => new CatchRuleset();
|
||||
}
|
||||
}
|
@ -8,7 +8,6 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.MathUtils;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
@ -17,6 +16,8 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
{
|
||||
public const int RNG_SEED = 1337;
|
||||
|
||||
public bool HardRockOffsets { get; set; }
|
||||
|
||||
public CatchBeatmapProcessor(IBeatmap beatmap)
|
||||
: base(beatmap)
|
||||
{
|
||||
@ -43,11 +44,10 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
}
|
||||
}
|
||||
|
||||
public static void ApplyPositionOffsets(IBeatmap beatmap, params Mod[] mods)
|
||||
public void ApplyPositionOffsets(IBeatmap beatmap)
|
||||
{
|
||||
var rng = new FastRandom(RNG_SEED);
|
||||
|
||||
bool shouldApplyHardRockOffset = mods.Any(m => m is ModHardRock);
|
||||
float? lastPosition = null;
|
||||
double lastStartTime = 0;
|
||||
|
||||
@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
switch (obj)
|
||||
{
|
||||
case Fruit fruit:
|
||||
if (shouldApplyHardRockOffset)
|
||||
if (HardRockOffsets)
|
||||
applyHardRockOffset(fruit, ref lastPosition, ref lastStartTime, rng);
|
||||
break;
|
||||
|
||||
|
@ -22,7 +22,9 @@ using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using System;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Game.Rulesets.Catch.Edit;
|
||||
using osu.Game.Rulesets.Catch.Skinning.Legacy;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch
|
||||
@ -175,12 +177,14 @@ namespace osu.Game.Rulesets.Catch
|
||||
|
||||
public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new CatchDifficultyCalculator(this, beatmap);
|
||||
|
||||
public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new CatchLegacySkinTransformer(source);
|
||||
public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new CatchLegacySkinTransformer(skin);
|
||||
|
||||
public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new CatchPerformanceCalculator(this, attributes, score);
|
||||
|
||||
public int LegacyID => 2;
|
||||
|
||||
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new CatchReplayFrame();
|
||||
|
||||
public override HitObjectComposer CreateHitObjectComposer() => new CatchHitObjectComposer(this);
|
||||
}
|
||||
}
|
||||
|
24
osu.Game.Rulesets.Catch/Edit/BananaShowerCompositionTool.cs
Normal file
24
osu.Game.Rulesets.Catch/Edit/BananaShowerCompositionTool.cs
Normal file
@ -0,0 +1,24 @@
|
||||
// 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.Graphics;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Edit.Blueprints;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Tools;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
public class BananaShowerCompositionTool : HitObjectCompositionTool
|
||||
{
|
||||
public BananaShowerCompositionTool()
|
||||
: base(nameof(BananaShower))
|
||||
{
|
||||
}
|
||||
|
||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners);
|
||||
|
||||
public override PlacementBlueprint CreatePlacementBlueprint() => new BananaShowerPlacementBlueprint();
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
// 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.Input.Events;
|
||||
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
{
|
||||
public class BananaShowerPlacementBlueprint : CatchPlacementBlueprint<BananaShower>
|
||||
{
|
||||
private readonly TimeSpanOutline outline;
|
||||
|
||||
public BananaShowerPlacementBlueprint()
|
||||
{
|
||||
InternalChild = outline = new TimeSpanOutline();
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
outline.UpdateFrom(HitObjectContainer, HitObject);
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
switch (PlacementActive)
|
||||
{
|
||||
case PlacementState.Waiting:
|
||||
if (e.Button != MouseButton.Left) break;
|
||||
|
||||
BeginPlacement(true);
|
||||
return true;
|
||||
|
||||
case PlacementState.Active:
|
||||
if (e.Button != MouseButton.Right) break;
|
||||
|
||||
// If the duration is negative, swap the start and the end time to make the duration positive.
|
||||
if (HitObject.Duration < 0)
|
||||
{
|
||||
HitObject.StartTime = HitObject.EndTime;
|
||||
HitObject.Duration = -HitObject.Duration;
|
||||
}
|
||||
|
||||
EndPlacement(HitObject.Duration > 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.OnMouseDown(e);
|
||||
}
|
||||
|
||||
public override void UpdateTimeAndPosition(SnapResult result)
|
||||
{
|
||||
base.UpdateTimeAndPosition(result);
|
||||
|
||||
if (!(result.Time is double time)) return;
|
||||
|
||||
switch (PlacementActive)
|
||||
{
|
||||
case PlacementState.Waiting:
|
||||
HitObject.StartTime = time;
|
||||
break;
|
||||
|
||||
case PlacementState.Active:
|
||||
HitObject.EndTime = time;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
{
|
||||
public class BananaShowerSelectionBlueprint : CatchSelectionBlueprint<BananaShower>
|
||||
{
|
||||
public BananaShowerSelectionBlueprint(BananaShower hitObject)
|
||||
: base(hitObject)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
{
|
||||
public class CatchPlacementBlueprint<THitObject> : PlacementBlueprint
|
||||
where THitObject : CatchHitObject, new()
|
||||
{
|
||||
protected new THitObject HitObject => (THitObject)base.HitObject;
|
||||
|
||||
protected ScrollingHitObjectContainer HitObjectContainer => (ScrollingHitObjectContainer)playfield.HitObjectContainer;
|
||||
|
||||
[Resolved]
|
||||
private Playfield playfield { get; set; }
|
||||
|
||||
public CatchPlacementBlueprint()
|
||||
: base(new THitObject())
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
{
|
||||
public abstract class CatchSelectionBlueprint<THitObject> : HitObjectSelectionBlueprint<THitObject>
|
||||
where THitObject : CatchHitObject
|
||||
{
|
||||
public override Vector2 ScreenSpaceSelectionPoint
|
||||
{
|
||||
get
|
||||
{
|
||||
float x = HitObject.OriginalX;
|
||||
float y = HitObjectContainer.PositionAtTime(HitObject.StartTime);
|
||||
return HitObjectContainer.ToScreenSpace(new Vector2(x, y + HitObjectContainer.DrawHeight));
|
||||
}
|
||||
}
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => SelectionQuad.Contains(screenSpacePos);
|
||||
|
||||
protected ScrollingHitObjectContainer HitObjectContainer => (ScrollingHitObjectContainer)playfield.HitObjectContainer;
|
||||
|
||||
[Resolved]
|
||||
private Playfield playfield { get; set; }
|
||||
|
||||
protected CatchSelectionBlueprint(THitObject hitObject)
|
||||
: base(hitObject)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.Skinning.Default;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
{
|
||||
public class FruitOutline : CompositeDrawable
|
||||
{
|
||||
public FruitOutline()
|
||||
{
|
||||
Anchor = Anchor.BottomLeft;
|
||||
Origin = Anchor.Centre;
|
||||
Size = new Vector2(2 * CatchHitObject.OBJECT_RADIUS);
|
||||
InternalChild = new BorderPiece();
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour osuColour)
|
||||
{
|
||||
Colour = osuColour.Yellow;
|
||||
}
|
||||
|
||||
public void UpdateFrom(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject hitObject)
|
||||
{
|
||||
X = hitObject.EffectiveX;
|
||||
Y = hitObjectContainer.PositionAtTime(hitObject.StartTime);
|
||||
Scale = new Vector2(hitObject.Scale);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
// 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.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
{
|
||||
public class TimeSpanOutline : CompositeDrawable
|
||||
{
|
||||
private const float border_width = 4;
|
||||
|
||||
private const float opacity_when_empty = 0.5f;
|
||||
|
||||
private bool isEmpty = true;
|
||||
|
||||
public TimeSpanOutline()
|
||||
{
|
||||
Anchor = Origin = Anchor.BottomLeft;
|
||||
RelativeSizeAxes = Axes.X;
|
||||
|
||||
Masking = true;
|
||||
BorderThickness = border_width;
|
||||
Alpha = opacity_when_empty;
|
||||
|
||||
// A box is needed to make the border visible.
|
||||
InternalChild = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.Transparent
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour osuColour)
|
||||
{
|
||||
BorderColour = osuColour.Yellow;
|
||||
}
|
||||
|
||||
public void UpdateFrom(ScrollingHitObjectContainer hitObjectContainer, BananaShower hitObject)
|
||||
{
|
||||
float startY = hitObjectContainer.PositionAtTime(hitObject.StartTime);
|
||||
float endY = hitObjectContainer.PositionAtTime(hitObject.EndTime);
|
||||
|
||||
Y = Math.Max(startY, endY);
|
||||
float height = Math.Abs(startY - endY);
|
||||
|
||||
bool wasEmpty = isEmpty;
|
||||
isEmpty = height == 0;
|
||||
if (wasEmpty != isEmpty)
|
||||
this.FadeTo(isEmpty ? opacity_when_empty : 1f, 150);
|
||||
|
||||
Height = Math.Max(height, border_width);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
// 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.Input.Events;
|
||||
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
{
|
||||
public class FruitPlacementBlueprint : CatchPlacementBlueprint<Fruit>
|
||||
{
|
||||
private readonly FruitOutline outline;
|
||||
|
||||
public FruitPlacementBlueprint()
|
||||
{
|
||||
InternalChild = outline = new FruitOutline();
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
BeginPlacement();
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
outline.UpdateFrom(HitObjectContainer, HitObject);
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
if (e.Button != MouseButton.Left) return base.OnMouseDown(e);
|
||||
|
||||
EndPlacement(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
public override void UpdateTimeAndPosition(SnapResult result)
|
||||
{
|
||||
base.UpdateTimeAndPosition(result);
|
||||
|
||||
HitObject.X = ToLocalSpace(result.ScreenSpacePosition).X;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
{
|
||||
public class FruitSelectionBlueprint : CatchSelectionBlueprint<Fruit>
|
||||
{
|
||||
private readonly FruitOutline outline;
|
||||
|
||||
public FruitSelectionBlueprint(Fruit hitObject)
|
||||
: base(hitObject)
|
||||
{
|
||||
InternalChild = outline = new FruitOutline();
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (IsSelected)
|
||||
outline.UpdateFrom(HitObjectContainer, HitObject);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
{
|
||||
public class JuiceStreamSelectionBlueprint : CatchSelectionBlueprint<JuiceStream>
|
||||
{
|
||||
public override Quad SelectionQuad => HitObjectContainer.ToScreenSpace(getBoundingBox().Offset(new Vector2(0, HitObjectContainer.DrawHeight)));
|
||||
|
||||
private float minNestedX;
|
||||
private float maxNestedX;
|
||||
|
||||
public JuiceStreamSelectionBlueprint(JuiceStream hitObject)
|
||||
: base(hitObject)
|
||||
{
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
HitObject.DefaultsApplied += onDefaultsApplied;
|
||||
computeObjectBounds();
|
||||
}
|
||||
|
||||
private void onDefaultsApplied(HitObject _) => computeObjectBounds();
|
||||
|
||||
private void computeObjectBounds()
|
||||
{
|
||||
minNestedX = HitObject.NestedHitObjects.OfType<CatchHitObject>().Min(nested => nested.OriginalX) - HitObject.OriginalX;
|
||||
maxNestedX = HitObject.NestedHitObjects.OfType<CatchHitObject>().Max(nested => nested.OriginalX) - HitObject.OriginalX;
|
||||
}
|
||||
|
||||
private RectangleF getBoundingBox()
|
||||
{
|
||||
float left = HitObject.OriginalX + minNestedX;
|
||||
float right = HitObject.OriginalX + maxNestedX;
|
||||
float top = HitObjectContainer.PositionAtTime(HitObject.EndTime);
|
||||
float bottom = HitObjectContainer.PositionAtTime(HitObject.StartTime);
|
||||
float objectRadius = CatchHitObject.OBJECT_RADIUS * HitObject.Scale;
|
||||
return new RectangleF(left, top, right - left, bottom - top).Inflate(objectRadius);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
HitObject.DefaultsApplied -= onDefaultsApplied;
|
||||
}
|
||||
}
|
||||
}
|
38
osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs
Normal file
38
osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs
Normal file
@ -0,0 +1,38 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Catch.Edit.Blueprints;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
public class CatchBlueprintContainer : ComposeBlueprintContainer
|
||||
{
|
||||
public CatchBlueprintContainer(CatchHitObjectComposer composer)
|
||||
: base(composer)
|
||||
{
|
||||
}
|
||||
|
||||
protected override SelectionHandler<HitObject> CreateSelectionHandler() => new CatchSelectionHandler();
|
||||
|
||||
public override HitObjectSelectionBlueprint CreateHitObjectBlueprintFor(HitObject hitObject)
|
||||
{
|
||||
switch (hitObject)
|
||||
{
|
||||
case Fruit fruit:
|
||||
return new FruitSelectionBlueprint(fruit);
|
||||
|
||||
case JuiceStream juiceStream:
|
||||
return new JuiceStreamSelectionBlueprint(juiceStream);
|
||||
|
||||
case BananaShower bananaShower:
|
||||
return new BananaShowerSelectionBlueprint(bananaShower);
|
||||
}
|
||||
|
||||
return base.CreateHitObjectBlueprintFor(hitObject);
|
||||
}
|
||||
}
|
||||
}
|
27
osu.Game.Rulesets.Catch/Edit/CatchEditorPlayfield.cs
Normal file
27
osu.Game.Rulesets.Catch/Edit/CatchEditorPlayfield.cs
Normal file
@ -0,0 +1,27 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
public class CatchEditorPlayfield : CatchPlayfield
|
||||
{
|
||||
// TODO fixme: the size of the catcher is not changed when circle size is changed in setup screen.
|
||||
public CatchEditorPlayfield(BeatmapDifficulty difficulty)
|
||||
: base(difficulty)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
// TODO: honor "hit animation" setting?
|
||||
CatcherArea.MovableCatcher.CatchFruitOnPlate = false;
|
||||
|
||||
// TODO: disable hit lighting as well
|
||||
}
|
||||
}
|
||||
}
|
42
osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
Normal file
42
osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
Normal file
@ -0,0 +1,42 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Tools;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
public class CatchHitObjectComposer : HitObjectComposer<CatchHitObject>
|
||||
{
|
||||
public CatchHitObjectComposer(CatchRuleset ruleset)
|
||||
: base(ruleset)
|
||||
{
|
||||
}
|
||||
|
||||
protected override DrawableRuleset<CatchHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null) =>
|
||||
new DrawableCatchEditorRuleset(ruleset, beatmap, mods);
|
||||
|
||||
protected override IReadOnlyList<HitObjectCompositionTool> CompositionTools => new HitObjectCompositionTool[]
|
||||
{
|
||||
new FruitCompositionTool(),
|
||||
new BananaShowerCompositionTool()
|
||||
};
|
||||
|
||||
public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition)
|
||||
{
|
||||
var result = base.SnapScreenSpacePositionToValidTime(screenSpacePosition);
|
||||
// TODO: implement position snap
|
||||
result.ScreenSpacePosition.X = screenSpacePosition.X;
|
||||
return result;
|
||||
}
|
||||
|
||||
protected override ComposeBlueprintContainer CreateBlueprintContainer() => new CatchBlueprintContainer(this);
|
||||
}
|
||||
}
|
46
osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs
Normal file
46
osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs
Normal file
@ -0,0 +1,46 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
public class CatchSelectionHandler : EditorSelectionHandler
|
||||
{
|
||||
protected ScrollingHitObjectContainer HitObjectContainer => (ScrollingHitObjectContainer)playfield.HitObjectContainer;
|
||||
|
||||
[Resolved]
|
||||
private Playfield playfield { get; set; }
|
||||
|
||||
public override bool HandleMovement(MoveSelectionEvent<HitObject> moveEvent)
|
||||
{
|
||||
var blueprint = moveEvent.Blueprint;
|
||||
Vector2 originalPosition = HitObjectContainer.ToLocalSpace(blueprint.ScreenSpaceSelectionPoint);
|
||||
Vector2 targetPosition = HitObjectContainer.ToLocalSpace(blueprint.ScreenSpaceSelectionPoint + moveEvent.ScreenSpaceDelta);
|
||||
float deltaX = targetPosition.X - originalPosition.X;
|
||||
|
||||
EditorBeatmap.PerformOnSelection(h =>
|
||||
{
|
||||
if (!(h is CatchHitObject hitObject)) return;
|
||||
|
||||
if (hitObject is BananaShower) return;
|
||||
|
||||
// TODO: confine in bounds
|
||||
hitObject.OriginalXBindable.Value += deltaX;
|
||||
|
||||
// Move the nested hit objects to give an instant result before nested objects are recreated.
|
||||
foreach (var nested in hitObject.NestedHitObjects.OfType<CatchHitObject>())
|
||||
nested.OriginalXBindable.Value += deltaX;
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
21
osu.Game.Rulesets.Catch/Edit/DrawableCatchEditorRuleset.cs
Normal file
21
osu.Game.Rulesets.Catch/Edit/DrawableCatchEditorRuleset.cs
Normal file
@ -0,0 +1,21 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
public class DrawableCatchEditorRuleset : DrawableCatchRuleset
|
||||
{
|
||||
public DrawableCatchEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
|
||||
: base(ruleset, beatmap, mods)
|
||||
{
|
||||
}
|
||||
|
||||
protected override Playfield CreatePlayfield() => new CatchEditorPlayfield(Beatmap.BeatmapInfo.BaseDifficulty);
|
||||
}
|
||||
}
|
24
osu.Game.Rulesets.Catch/Edit/FruitCompositionTool.cs
Normal file
24
osu.Game.Rulesets.Catch/Edit/FruitCompositionTool.cs
Normal file
@ -0,0 +1,24 @@
|
||||
// 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.Graphics;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Edit.Blueprints;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Tools;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
public class FruitCompositionTool : HitObjectCompositionTool
|
||||
{
|
||||
public FruitCompositionTool()
|
||||
: base(nameof(Fruit))
|
||||
{
|
||||
}
|
||||
|
||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles);
|
||||
|
||||
public override PlacementBlueprint CreatePlacementBlueprint() => new FruitPlacementBlueprint();
|
||||
}
|
||||
}
|
@ -5,11 +5,12 @@ using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Catch.Beatmaps;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
public class CatchModDifficultyAdjust : ModDifficultyAdjust
|
||||
public class CatchModDifficultyAdjust : ModDifficultyAdjust, IApplicableToBeatmapProcessor
|
||||
{
|
||||
[SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1)]
|
||||
public BindableNumber<float> CircleSize { get; } = new BindableFloatWithLimitExtension
|
||||
@ -31,6 +32,9 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
Value = 5,
|
||||
};
|
||||
|
||||
[SettingSource("Spicy Patterns", "Adjust the patterns as if Hard Rock is enabled.")]
|
||||
public BindableBool HardRockOffsets { get; } = new BindableBool();
|
||||
|
||||
protected override void ApplyLimits(bool extended)
|
||||
{
|
||||
base.ApplyLimits(extended);
|
||||
@ -45,12 +49,14 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
string circleSize = CircleSize.IsDefault ? string.Empty : $"CS {CircleSize.Value:N1}";
|
||||
string approachRate = ApproachRate.IsDefault ? string.Empty : $"AR {ApproachRate.Value:N1}";
|
||||
string spicyPatterns = HardRockOffsets.IsDefault ? string.Empty : "Spicy patterns";
|
||||
|
||||
return string.Join(", ", new[]
|
||||
{
|
||||
circleSize,
|
||||
base.SettingDescription,
|
||||
approachRate
|
||||
approachRate,
|
||||
spicyPatterns,
|
||||
}.Where(s => !string.IsNullOrEmpty(s)));
|
||||
}
|
||||
}
|
||||
@ -70,5 +76,11 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
ApplySetting(CircleSize, cs => difficulty.CircleSize = cs);
|
||||
ApplySetting(ApproachRate, ar => difficulty.ApproachRate = ar);
|
||||
}
|
||||
|
||||
public void ApplyToBeatmapProcessor(IBeatmapProcessor beatmapProcessor)
|
||||
{
|
||||
var catchProcessor = (CatchBeatmapProcessor)beatmapProcessor;
|
||||
catchProcessor.HardRockOffsets = HardRockOffsets.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,10 +7,14 @@ using osu.Game.Rulesets.Catch.Beatmaps;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
public class CatchModHardRock : ModHardRock, IApplicableToBeatmap
|
||||
public class CatchModHardRock : ModHardRock, IApplicableToBeatmapProcessor
|
||||
{
|
||||
public override double ScoreMultiplier => 1.12;
|
||||
|
||||
public void ApplyToBeatmap(IBeatmap beatmap) => CatchBeatmapProcessor.ApplyPositionOffsets(beatmap, this);
|
||||
public void ApplyToBeatmapProcessor(IBeatmapProcessor beatmapProcessor)
|
||||
{
|
||||
var catchProcessor = (CatchBeatmapProcessor)beatmapProcessor;
|
||||
catchProcessor.HardRockOffsets = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,8 +17,8 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
/// </summary>
|
||||
private bool providesComboCounter => this.HasFont(LegacyFont.Combo);
|
||||
|
||||
public CatchLegacySkinTransformer(ISkinSource source)
|
||||
: base(source)
|
||||
public CatchLegacySkinTransformer(ISkin skin)
|
||||
: base(skin)
|
||||
{
|
||||
}
|
||||
|
||||
@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
switch (targetComponent.Target)
|
||||
{
|
||||
case SkinnableTarget.MainHUDComponents:
|
||||
var components = Source.GetDrawableComponent(component) as SkinnableTargetComponentsContainer;
|
||||
var components = base.GetDrawableComponent(component) as SkinnableTargetComponentsContainer;
|
||||
|
||||
if (providesComboCounter && components != null)
|
||||
{
|
||||
@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
return null;
|
||||
|
||||
case CatchSkinComponents.Catcher:
|
||||
var version = Source.GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version)?.Value ?? 1;
|
||||
var version = GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version)?.Value ?? 1;
|
||||
|
||||
if (version < 2.3m)
|
||||
{
|
||||
@ -83,13 +83,13 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
|
||||
case CatchSkinComponents.CatchComboCounter:
|
||||
if (providesComboCounter)
|
||||
return new LegacyCatchComboCounter(Source);
|
||||
return new LegacyCatchComboCounter(Skin);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return Source.GetDrawableComponent(component);
|
||||
return base.GetDrawableComponent(component);
|
||||
}
|
||||
|
||||
public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
|
||||
@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
switch (lookup)
|
||||
{
|
||||
case CatchSkinColour colour:
|
||||
var result = (Bindable<Color4>)Source.GetConfig<SkinCustomColourLookup, TValue>(new SkinCustomColourLookup(colour));
|
||||
var result = (Bindable<Color4>)base.GetConfig<SkinCustomColourLookup, TValue>(new SkinCustomColourLookup(colour));
|
||||
if (result == null)
|
||||
return null;
|
||||
|
||||
@ -105,7 +105,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
return (IBindable<TValue>)result;
|
||||
}
|
||||
|
||||
return Source.GetConfig<TLookup, TValue>(lookup);
|
||||
return base.GetConfig<TLookup, TValue>(lookup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Mania
|
||||
|
||||
public override HitObjectComposer CreateHitObjectComposer() => new ManiaHitObjectComposer(this);
|
||||
|
||||
public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new ManiaLegacySkinTransformer(source, beatmap);
|
||||
public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new ManiaLegacySkinTransformer(skin, beatmap);
|
||||
|
||||
public override IEnumerable<Mod> ConvertFromLegacyMods(LegacyMods mods)
|
||||
{
|
||||
|
@ -50,29 +50,25 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
{ HitResult.Miss, "mania-hit0" }
|
||||
};
|
||||
|
||||
private Lazy<bool> isLegacySkin;
|
||||
private readonly Lazy<bool> isLegacySkin;
|
||||
|
||||
/// <summary>
|
||||
/// Whether texture for the keys exists.
|
||||
/// Used to determine if the mania ruleset is skinned.
|
||||
/// </summary>
|
||||
private Lazy<bool> hasKeyTexture;
|
||||
private readonly Lazy<bool> hasKeyTexture;
|
||||
|
||||
public ManiaLegacySkinTransformer(ISkinSource source, IBeatmap beatmap)
|
||||
: base(source)
|
||||
public ManiaLegacySkinTransformer(ISkin skin, IBeatmap beatmap)
|
||||
: base(skin)
|
||||
{
|
||||
this.beatmap = (ManiaBeatmap)beatmap;
|
||||
|
||||
Source.SourceChanged += sourceChanged;
|
||||
sourceChanged();
|
||||
}
|
||||
|
||||
private void sourceChanged()
|
||||
{
|
||||
isLegacySkin = new Lazy<bool>(() => FindProvider(s => s.GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version) != null) != null);
|
||||
hasKeyTexture = new Lazy<bool>(() => FindProvider(s => s.GetAnimation(
|
||||
s.GetManiaSkinConfig<string>(LegacyManiaSkinConfigurationLookups.KeyImage, 0)?.Value
|
||||
?? "mania-key1", true, true) != null) != null);
|
||||
isLegacySkin = new Lazy<bool>(() => GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version) != null);
|
||||
hasKeyTexture = new Lazy<bool>(() =>
|
||||
{
|
||||
var keyImage = this.GetManiaSkinConfig<string>(LegacyManiaSkinConfigurationLookups.KeyImage, 0)?.Value ?? "mania-key1";
|
||||
return this.GetAnimation(keyImage, true, true) != null;
|
||||
});
|
||||
}
|
||||
|
||||
public override Drawable GetDrawableComponent(ISkinComponent component)
|
||||
@ -125,7 +121,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
break;
|
||||
}
|
||||
|
||||
return Source.GetDrawableComponent(component);
|
||||
return base.GetDrawableComponent(component);
|
||||
}
|
||||
|
||||
private Drawable getResult(HitResult result)
|
||||
@ -146,15 +142,15 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample && legacySample.IsLayered)
|
||||
return new SampleVirtual();
|
||||
|
||||
return Source.GetSample(sampleInfo);
|
||||
return base.GetSample(sampleInfo);
|
||||
}
|
||||
|
||||
public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
|
||||
{
|
||||
if (lookup is ManiaSkinConfigurationLookup maniaLookup)
|
||||
return Source.GetConfig<LegacyManiaSkinConfigurationLookup, TValue>(new LegacyManiaSkinConfigurationLookup(beatmap.TotalColumns, maniaLookup.Lookup, maniaLookup.TargetColumn));
|
||||
return base.GetConfig<LegacyManiaSkinConfigurationLookup, TValue>(new LegacyManiaSkinConfigurationLookup(beatmap.TotalColumns, maniaLookup.Lookup, maniaLookup.TargetColumn));
|
||||
|
||||
return Source.GetConfig<TLookup, TValue>(lookup);
|
||||
return base.GetConfig<TLookup, TValue>(lookup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio.Sample;
|
||||
@ -112,7 +113,9 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => null;
|
||||
|
||||
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => null;
|
||||
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => lookupFunction(this) ? this : null;
|
||||
|
||||
public IEnumerable<ISkin> AllSources => new[] { this };
|
||||
|
||||
public event Action SourceChanged
|
||||
{
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
@ -164,9 +165,11 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
public ISample GetSample(ISampleInfo sampleInfo) => null;
|
||||
|
||||
public TValue GetValue<TConfiguration, TValue>(Func<TConfiguration, TValue> query) where TConfiguration : SkinConfiguration => default;
|
||||
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => null;
|
||||
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => null;
|
||||
|
||||
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => lookupFunction(this) ? this : null;
|
||||
|
||||
public IEnumerable<ISkin> AllSources => new[] { this };
|
||||
|
||||
public event Action SourceChanged;
|
||||
|
||||
|
@ -219,7 +219,7 @@ namespace osu.Game.Rulesets.Osu
|
||||
|
||||
public override RulesetSettingsSubsection CreateSettings() => new OsuSettingsSubsection(this);
|
||||
|
||||
public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new OsuLegacySkinTransformer(source);
|
||||
public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new OsuLegacySkinTransformer(skin);
|
||||
|
||||
public int LegacyID => 0;
|
||||
|
||||
|
@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
{
|
||||
public class OsuLegacySkinTransformer : LegacySkinTransformer
|
||||
{
|
||||
private Lazy<bool> hasHitCircle;
|
||||
private readonly Lazy<bool> hasHitCircle;
|
||||
|
||||
/// <summary>
|
||||
/// On osu-stable, hitcircles have 5 pixels of transparent padding on each side to allow for shadows etc.
|
||||
@ -20,16 +20,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
/// </summary>
|
||||
public const float LEGACY_CIRCLE_RADIUS = 64 - 5;
|
||||
|
||||
public OsuLegacySkinTransformer(ISkinSource source)
|
||||
: base(source)
|
||||
public OsuLegacySkinTransformer(ISkin skin)
|
||||
: base(skin)
|
||||
{
|
||||
Source.SourceChanged += sourceChanged;
|
||||
sourceChanged();
|
||||
}
|
||||
|
||||
private void sourceChanged()
|
||||
{
|
||||
hasHitCircle = new Lazy<bool>(() => FindProvider(s => s.GetTexture("hitcircle") != null) != null);
|
||||
hasHitCircle = new Lazy<bool>(() => GetTexture("hitcircle") != null);
|
||||
}
|
||||
|
||||
public override Drawable GetDrawableComponent(ISkinComponent component)
|
||||
@ -49,16 +43,13 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
return followCircle;
|
||||
|
||||
case OsuSkinComponents.SliderBall:
|
||||
// specular and nd layers must come from the same source as the ball texure.
|
||||
var ballProvider = Source.FindProvider(s => s.GetTexture("sliderb") != null || s.GetTexture("sliderb0") != null);
|
||||
|
||||
var sliderBallContent = ballProvider.GetAnimation("sliderb", true, true, animationSeparator: "");
|
||||
var sliderBallContent = this.GetAnimation("sliderb", true, true, animationSeparator: "");
|
||||
|
||||
// todo: slider ball has a custom frame delay based on velocity
|
||||
// Math.Max((150 / Velocity) * GameBase.SIXTY_FRAME_TIME, GameBase.SIXTY_FRAME_TIME);
|
||||
|
||||
if (sliderBallContent != null)
|
||||
return new LegacySliderBall(sliderBallContent, ballProvider);
|
||||
return new LegacySliderBall(sliderBallContent, this);
|
||||
|
||||
return null;
|
||||
|
||||
@ -87,18 +78,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
return null;
|
||||
|
||||
case OsuSkinComponents.Cursor:
|
||||
var cursorProvider = Source.FindProvider(s => s.GetTexture("cursor") != null);
|
||||
|
||||
if (cursorProvider != null)
|
||||
return new LegacyCursor(cursorProvider);
|
||||
if (GetTexture("cursor") != null)
|
||||
return new LegacyCursor(this);
|
||||
|
||||
return null;
|
||||
|
||||
case OsuSkinComponents.CursorTrail:
|
||||
var trailProvider = Source.FindProvider(s => s.GetTexture("cursortrail") != null);
|
||||
|
||||
if (trailProvider != null)
|
||||
return new LegacyCursorTrail(trailProvider);
|
||||
if (GetTexture("cursortrail") != null)
|
||||
return new LegacyCursorTrail(this);
|
||||
|
||||
return null;
|
||||
|
||||
@ -113,9 +100,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
};
|
||||
|
||||
case OsuSkinComponents.SpinnerBody:
|
||||
bool hasBackground = Source.GetTexture("spinner-background") != null;
|
||||
bool hasBackground = GetTexture("spinner-background") != null;
|
||||
|
||||
if (Source.GetTexture("spinner-top") != null && !hasBackground)
|
||||
if (GetTexture("spinner-top") != null && !hasBackground)
|
||||
return new LegacyNewStyleSpinner();
|
||||
else if (hasBackground)
|
||||
return new LegacyOldStyleSpinner();
|
||||
@ -124,7 +111,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
}
|
||||
}
|
||||
|
||||
return Source.GetDrawableComponent(component);
|
||||
return base.GetDrawableComponent(component);
|
||||
}
|
||||
|
||||
public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
|
||||
@ -132,7 +119,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
switch (lookup)
|
||||
{
|
||||
case OsuSkinColour colour:
|
||||
return Source.GetConfig<SkinCustomColourLookup, TValue>(new SkinCustomColourLookup(colour));
|
||||
return base.GetConfig<SkinCustomColourLookup, TValue>(new SkinCustomColourLookup(colour));
|
||||
|
||||
case OsuSkinConfiguration osuLookup:
|
||||
switch (osuLookup)
|
||||
@ -146,14 +133,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
case OsuSkinConfiguration.HitCircleOverlayAboveNumber:
|
||||
// See https://osu.ppy.sh/help/wiki/Skinning/skin.ini#%5Bgeneral%5D
|
||||
// HitCircleOverlayAboveNumer (with typo) should still be supported for now.
|
||||
return Source.GetConfig<OsuSkinConfiguration, TValue>(OsuSkinConfiguration.HitCircleOverlayAboveNumber) ??
|
||||
Source.GetConfig<OsuSkinConfiguration, TValue>(OsuSkinConfiguration.HitCircleOverlayAboveNumer);
|
||||
return base.GetConfig<OsuSkinConfiguration, TValue>(OsuSkinConfiguration.HitCircleOverlayAboveNumber) ??
|
||||
base.GetConfig<OsuSkinConfiguration, TValue>(OsuSkinConfiguration.HitCircleOverlayAboveNumer);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return Source.GetConfig<TLookup, TValue>(lookup);
|
||||
return base.GetConfig<TLookup, TValue>(lookup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
@ -15,18 +14,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
{
|
||||
public class TaikoLegacySkinTransformer : LegacySkinTransformer
|
||||
{
|
||||
private Lazy<bool> hasExplosion;
|
||||
private readonly Lazy<bool> hasExplosion;
|
||||
|
||||
public TaikoLegacySkinTransformer(ISkinSource source)
|
||||
: base(source)
|
||||
public TaikoLegacySkinTransformer(ISkin skin)
|
||||
: base(skin)
|
||||
{
|
||||
Source.SourceChanged += sourceChanged;
|
||||
sourceChanged();
|
||||
}
|
||||
|
||||
private void sourceChanged()
|
||||
{
|
||||
hasExplosion = new Lazy<bool>(() => Source.GetTexture(getHitName(TaikoSkinComponents.TaikoExplosionGreat)) != null);
|
||||
hasExplosion = new Lazy<bool>(() => GetTexture(getHitName(TaikoSkinComponents.TaikoExplosionGreat)) != null);
|
||||
}
|
||||
|
||||
public override Drawable GetDrawableComponent(ISkinComponent component)
|
||||
@ -56,7 +49,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
|
||||
case TaikoSkinComponents.CentreHit:
|
||||
case TaikoSkinComponents.RimHit:
|
||||
|
||||
if (GetTexture("taikohitcircle") != null)
|
||||
return new LegacyHit(taikoComponent.Component);
|
||||
|
||||
@ -91,7 +83,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
return null;
|
||||
|
||||
case TaikoSkinComponents.TaikoExplosionMiss:
|
||||
|
||||
var missSprite = this.GetAnimation(getHitName(taikoComponent.Component), true, false);
|
||||
if (missSprite != null)
|
||||
return new LegacyHitExplosion(missSprite);
|
||||
@ -100,7 +91,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
|
||||
case TaikoSkinComponents.TaikoExplosionOk:
|
||||
case TaikoSkinComponents.TaikoExplosionGreat:
|
||||
|
||||
var hitName = getHitName(taikoComponent.Component);
|
||||
var hitSprite = this.GetAnimation(hitName, true, false);
|
||||
|
||||
@ -132,7 +122,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
}
|
||||
}
|
||||
|
||||
return Source.GetDrawableComponent(component);
|
||||
return base.GetDrawableComponent(component);
|
||||
}
|
||||
|
||||
private string getHitName(TaikoSkinComponents component)
|
||||
@ -155,13 +145,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
public override ISample GetSample(ISampleInfo sampleInfo)
|
||||
{
|
||||
if (sampleInfo is HitSampleInfo hitSampleInfo)
|
||||
return Source.GetSample(new LegacyTaikoSampleInfo(hitSampleInfo));
|
||||
return base.GetSample(new LegacyTaikoSampleInfo(hitSampleInfo));
|
||||
|
||||
return base.GetSample(sampleInfo);
|
||||
}
|
||||
|
||||
public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => Source.GetConfig<TLookup, TValue>(lookup);
|
||||
|
||||
private class LegacyTaikoSampleInfo : HitSampleInfo
|
||||
{
|
||||
public LegacyTaikoSampleInfo(HitSampleInfo sampleInfo)
|
||||
|
@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Taiko
|
||||
|
||||
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new TaikoBeatmapConverter(beatmap, this);
|
||||
|
||||
public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new TaikoLegacySkinTransformer(source);
|
||||
public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new TaikoLegacySkinTransformer(skin);
|
||||
|
||||
public const string SHORT_NAME = "taiko";
|
||||
|
||||
|
@ -14,6 +14,14 @@ namespace osu.Game.Tests.Mods
|
||||
[TestFixture]
|
||||
public class ModUtilsTest
|
||||
{
|
||||
[Test]
|
||||
public void TestModIsNotCompatibleWithItself()
|
||||
{
|
||||
var mod = new Mock<CustomMod1>();
|
||||
Assert.That(ModUtils.CheckCompatibleSet(new[] { mod.Object, mod.Object }, out var invalid), Is.False);
|
||||
Assert.That(invalid, Is.EquivalentTo(new[] { mod.Object }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestModIsCompatibleByItself()
|
||||
{
|
||||
@ -147,7 +155,7 @@ namespace osu.Game.Tests.Mods
|
||||
// multi mod.
|
||||
new object[]
|
||||
{
|
||||
new Mod[] { new MultiMod(new OsuModHalfTime()), new OsuModHalfTime() },
|
||||
new Mod[] { new MultiMod(new OsuModHalfTime()), new OsuModDaycore() },
|
||||
new[] { typeof(MultiMod) }
|
||||
},
|
||||
// valid pair.
|
||||
|
@ -55,7 +55,12 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
[Test]
|
||||
public void TestExitWithoutSave()
|
||||
{
|
||||
AddStep("exit without save", () => Editor.Exit());
|
||||
AddStep("exit without save", () =>
|
||||
{
|
||||
Editor.Exit();
|
||||
DialogOverlay.CurrentDialog.PerformOkAction();
|
||||
});
|
||||
|
||||
AddUntilStep("wait for exit", () => !Editor.IsCurrentScreen());
|
||||
AddAssert("new beatmap not persisted", () => beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID)?.DeletePending == true);
|
||||
}
|
||||
|
@ -116,12 +116,12 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
private class TestOsuRuleset : OsuRuleset
|
||||
{
|
||||
public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new TestOsuLegacySkinTransformer(source);
|
||||
public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new TestOsuLegacySkinTransformer(skin);
|
||||
|
||||
private class TestOsuLegacySkinTransformer : OsuLegacySkinTransformer
|
||||
{
|
||||
public TestOsuLegacySkinTransformer(ISkinSource source)
|
||||
: base(source)
|
||||
public TestOsuLegacySkinTransformer(ISkin skin)
|
||||
: base(skin)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
@ -167,7 +168,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
public void Disable()
|
||||
{
|
||||
allow = false;
|
||||
TriggerSourceChanged();
|
||||
OnSourceChanged();
|
||||
}
|
||||
|
||||
public SwitchableSkinProvidingContainer(ISkin skin)
|
||||
@ -330,6 +331,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => throw new NotImplementedException();
|
||||
|
||||
public IEnumerable<ISkin> AllSources => throw new NotImplementedException();
|
||||
|
||||
public event Action SourceChanged
|
||||
{
|
||||
add { }
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
@ -146,7 +147,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => source?.GetTexture(componentName, wrapModeS, wrapModeT);
|
||||
public ISample GetSample(ISampleInfo sampleInfo) => source?.GetSample(sampleInfo);
|
||||
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => source?.GetConfig<TLookup, TValue>(lookup);
|
||||
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => source?.FindProvider(lookupFunction);
|
||||
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => lookupFunction(this) ? this : source?.FindProvider(lookupFunction);
|
||||
public IEnumerable<ISkin> AllSources => new[] { this }.Concat(source?.AllSources ?? Enumerable.Empty<ISkin>());
|
||||
|
||||
public void TriggerSourceChanged()
|
||||
{
|
||||
|
@ -6,15 +6,20 @@ using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays.Mods;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Screens.OnlinePlay.Components;
|
||||
using osu.Game.Screens.OnlinePlay.Match.Components;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
|
||||
using osu.Game.Tests.Resources;
|
||||
@ -159,6 +164,50 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddUntilStep("play started", () => !multiplayerScreen.IsCurrentScreen());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLeaveNavigation()
|
||||
{
|
||||
loadMultiplayer();
|
||||
|
||||
createRoom(() => new Room
|
||||
{
|
||||
Name = { Value = "Test Room" },
|
||||
Playlist =
|
||||
{
|
||||
new PlaylistItem
|
||||
{
|
||||
Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
|
||||
Ruleset = { Value = new OsuRuleset().RulesetInfo },
|
||||
AllowedMods = { new OsuModHidden() }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
AddStep("open mod overlay", () => this.ChildrenOfType<PurpleTriangleButton>().ElementAt(2).Click());
|
||||
|
||||
AddStep("invoke on back button", () => multiplayerScreen.OnBackButton());
|
||||
|
||||
AddAssert("mod overlay is hidden", () => this.ChildrenOfType<LocalPlayerModSelectOverlay>().Single().State.Value == Visibility.Hidden);
|
||||
|
||||
AddAssert("dialog overlay is hidden", () => DialogOverlay.State.Value == Visibility.Hidden);
|
||||
|
||||
testLeave("lounge tab item", () => this.ChildrenOfType<BreadcrumbControl<IScreen>.BreadcrumbTabItem>().First().Click());
|
||||
|
||||
testLeave("back button", () => multiplayerScreen.OnBackButton());
|
||||
|
||||
// mimics home button and OS window close
|
||||
testLeave("forced exit", () => multiplayerScreen.Exit());
|
||||
|
||||
void testLeave(string actionName, Action action)
|
||||
{
|
||||
AddStep($"leave via {actionName}", action);
|
||||
|
||||
AddAssert("dialog overlay is visible", () => DialogOverlay.State.Value == Visibility.Visible);
|
||||
|
||||
AddStep("close dialog overlay", () => InputManager.Key(Key.Escape));
|
||||
}
|
||||
}
|
||||
|
||||
private void createRoom(Func<Room> room)
|
||||
{
|
||||
AddStep("open room", () =>
|
||||
|
@ -133,6 +133,9 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
IBeatmapProcessor processor = rulesetInstance.CreateBeatmapProcessor(converted);
|
||||
|
||||
foreach (var mod in mods.OfType<IApplicableToBeatmapProcessor>())
|
||||
mod.ApplyToBeatmapProcessor(processor);
|
||||
|
||||
processor?.PreProcess();
|
||||
|
||||
// Compute default values for hitobjects, including creating nested hitobjects in-case they're needed
|
||||
|
@ -20,7 +20,8 @@ namespace osu.Game.Graphics.Containers.Markdown
|
||||
|
||||
public override MarkdownTextFlowContainer CreateTextFlow() => new HeadingTextFlowContainer
|
||||
{
|
||||
Weight = GetFontWeightByLevel(level),
|
||||
FontSize = GetFontSizeByLevel(level),
|
||||
FontWeight = GetFontWeightByLevel(level),
|
||||
};
|
||||
|
||||
protected override float GetFontSizeByLevel(int level)
|
||||
@ -28,27 +29,25 @@ namespace osu.Game.Graphics.Containers.Markdown
|
||||
// Reference for this font size
|
||||
// https://github.com/ppy/osu-web/blob/376cac43a051b9c85ce95e2c446099be187b3e45/resources/assets/less/bem/osu-md.less#L9
|
||||
// https://github.com/ppy/osu-web/blob/376cac43a051b9c85ce95e2c446099be187b3e45/resources/assets/less/variables.less#L161
|
||||
const float base_font_size = 14;
|
||||
|
||||
switch (level)
|
||||
{
|
||||
case 1:
|
||||
return 30 / base_font_size;
|
||||
return 30;
|
||||
|
||||
case 2:
|
||||
return 26 / base_font_size;
|
||||
return 26;
|
||||
|
||||
case 3:
|
||||
return 20 / base_font_size;
|
||||
return 20;
|
||||
|
||||
case 4:
|
||||
return 18 / base_font_size;
|
||||
return 18;
|
||||
|
||||
case 5:
|
||||
return 16 / base_font_size;
|
||||
return 16;
|
||||
|
||||
default:
|
||||
return 1;
|
||||
return 14;
|
||||
}
|
||||
}
|
||||
|
||||
@ -67,9 +66,11 @@ namespace osu.Game.Graphics.Containers.Markdown
|
||||
|
||||
private class HeadingTextFlowContainer : OsuMarkdownTextFlowContainer
|
||||
{
|
||||
public FontWeight Weight { get; set; }
|
||||
public float FontSize;
|
||||
public FontWeight FontWeight;
|
||||
|
||||
protected override SpriteText CreateSpriteText() => base.CreateSpriteText().With(t => t.Font = t.Font.With(weight: Weight));
|
||||
protected override SpriteText CreateSpriteText()
|
||||
=> base.CreateSpriteText().With(t => t.Font = t.Font.With(size: FontSize, weight: FontWeight));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System.Linq;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Screens;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterface
|
||||
@ -19,8 +20,13 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
if (stack.CurrentScreen != null)
|
||||
onPushed(null, stack.CurrentScreen);
|
||||
}
|
||||
|
||||
Current.ValueChanged += current => current.NewValue.MakeCurrent();
|
||||
protected override void SelectTab(TabItem<IScreen> tab)
|
||||
{
|
||||
// override base method to prevent current item from being changed on click.
|
||||
// depend on screen push/exit to change current item instead.
|
||||
tab.Value.MakeCurrent();
|
||||
}
|
||||
|
||||
private void onPushed(IScreen lastScreen, IScreen newScreen)
|
||||
|
@ -261,7 +261,7 @@ namespace osu.Game
|
||||
dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, API, contextFactory, Host, () => difficultyCache, LocalConfig));
|
||||
dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, contextFactory, RulesetStore, API, Audio, Resources, Host, defaultBeatmap, true));
|
||||
|
||||
// this should likely be moved to ArchiveModelManager when another case appers where it is necessary
|
||||
// this should likely be moved to ArchiveModelManager when another case appears where it is necessary
|
||||
// to have inter-dependent model managers. this could be obtained with an IHasForeign<T> interface to
|
||||
// allow lookups to be done on the child (ScoreManager in this case) to perform the cascading delete.
|
||||
List<ScoreInfo> getBeatmapScores(BeatmapSetInfo set)
|
||||
|
@ -39,9 +39,9 @@ namespace osu.Game.Overlays
|
||||
protected OverlayStreamItem(T value)
|
||||
: base(value)
|
||||
{
|
||||
Height = 60;
|
||||
Width = 100;
|
||||
Padding = new MarginPadding(5);
|
||||
Height = 50;
|
||||
Width = 90;
|
||||
Margin = new MarginPadding(5);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
|
18
osu.Game/Rulesets/Mods/IApplicableToBeatmapProcessor.cs
Normal file
18
osu.Game/Rulesets/Mods/IApplicableToBeatmapProcessor.cs
Normal file
@ -0,0 +1,18 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Beatmaps;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for a <see cref="Mod"/> that applies changes to a <see cref="BeatmapProcessor"/>.
|
||||
/// </summary>
|
||||
public interface IApplicableToBeatmapProcessor : IApplicableMod
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies this <see cref="Mod"/> to a <see cref="BeatmapProcessor"/>.
|
||||
/// </summary>
|
||||
void ApplyToBeatmapProcessor(IBeatmapProcessor beatmapProcessor);
|
||||
}
|
||||
}
|
12
osu.Game/Rulesets/Mods/IHasSeed.cs
Normal file
12
osu.Game/Rulesets/Mods/IHasSeed.cs
Normal file
@ -0,0 +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.
|
||||
|
||||
using osu.Framework.Bindables;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
public interface IHasSeed
|
||||
{
|
||||
Bindable<int?> Seed { get; }
|
||||
}
|
||||
}
|
@ -2,17 +2,13 @@
|
||||
// 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.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Overlays.Settings;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
public abstract class ModRandom : Mod
|
||||
public abstract class ModRandom : Mod, IHasSeed
|
||||
{
|
||||
public override string Name => "Random";
|
||||
public override string Acronym => "RD";
|
||||
@ -20,88 +16,11 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override IconUsage? Icon => OsuIcon.Dice;
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
[SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(ModRandomSettingsControl))]
|
||||
[SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(SeedSettingsControl))]
|
||||
public Bindable<int?> Seed { get; } = new Bindable<int?>
|
||||
{
|
||||
Default = null,
|
||||
Value = null
|
||||
};
|
||||
|
||||
private class ModRandomSettingsControl : SettingsItem<int?>
|
||||
{
|
||||
protected override Drawable CreateControl() => new SeedControl
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Margin = new MarginPadding { Top = 5 }
|
||||
};
|
||||
|
||||
private sealed class SeedControl : CompositeDrawable, IHasCurrentValue<int?>
|
||||
{
|
||||
private readonly BindableWithCurrent<int?> current = new BindableWithCurrent<int?>();
|
||||
|
||||
public Bindable<int?> Current
|
||||
{
|
||||
get => current;
|
||||
set
|
||||
{
|
||||
current.Current = value;
|
||||
seedNumberBox.Text = value.Value.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
private readonly SettingsNumberBox.NumberBox seedNumberBox;
|
||||
|
||||
public SeedControl()
|
||||
{
|
||||
AutoSizeAxes = Axes.Y;
|
||||
|
||||
InternalChildren = new[]
|
||||
{
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.Absolute, 2),
|
||||
new Dimension(GridSizeMode.Relative, 0.25f)
|
||||
},
|
||||
RowDimensions = new[]
|
||||
{
|
||||
new Dimension(GridSizeMode.AutoSize)
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
seedNumberBox = new SettingsNumberBox.NumberBox
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
CommitOnFocusLost = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
seedNumberBox.Current.BindValueChanged(e =>
|
||||
{
|
||||
int? value = null;
|
||||
|
||||
if (int.TryParse(e.NewValue, out var intVal))
|
||||
value = intVal;
|
||||
|
||||
current.Value = value;
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
if (current.Value == null)
|
||||
seedNumberBox.Text = current.Current.Value.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
92
osu.Game/Rulesets/Mods/SeedSettingsControl.cs
Normal file
92
osu.Game/Rulesets/Mods/SeedSettingsControl.cs
Normal file
@ -0,0 +1,92 @@
|
||||
// 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.Framework.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays.Settings;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
/// <summary>
|
||||
/// A settings control for use by <see cref="IHasSeed"/> mods which have a customisable seed value.
|
||||
/// </summary>
|
||||
public class SeedSettingsControl : SettingsItem<int?>
|
||||
{
|
||||
protected override Drawable CreateControl() => new SeedControl
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Margin = new MarginPadding { Top = 5 }
|
||||
};
|
||||
|
||||
private sealed class SeedControl : CompositeDrawable, IHasCurrentValue<int?>
|
||||
{
|
||||
private readonly BindableWithCurrent<int?> current = new BindableWithCurrent<int?>();
|
||||
|
||||
public Bindable<int?> Current
|
||||
{
|
||||
get => current;
|
||||
set
|
||||
{
|
||||
current.Current = value;
|
||||
seedNumberBox.Text = value.Value.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
private readonly OsuNumberBox seedNumberBox;
|
||||
|
||||
public SeedControl()
|
||||
{
|
||||
AutoSizeAxes = Axes.Y;
|
||||
|
||||
InternalChildren = new[]
|
||||
{
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.Absolute, 2),
|
||||
new Dimension(GridSizeMode.Relative, 0.25f)
|
||||
},
|
||||
RowDimensions = new[]
|
||||
{
|
||||
new Dimension(GridSizeMode.AutoSize)
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
seedNumberBox = new OsuNumberBox
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
CommitOnFocusLost = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
seedNumberBox.Current.BindValueChanged(e =>
|
||||
{
|
||||
int? value = null;
|
||||
|
||||
if (int.TryParse(e.NewValue, out var intVal))
|
||||
value = intVal;
|
||||
|
||||
current.Value = value;
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
if (current.Value == null)
|
||||
seedNumberBox.Text = current.Current.Value.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -127,7 +127,7 @@ namespace osu.Game.Rulesets
|
||||
[CanBeNull]
|
||||
public ModAutoplay GetAutoplayMod() => GetAllMods().OfType<ModAutoplay>().FirstOrDefault();
|
||||
|
||||
public virtual ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => null;
|
||||
public virtual ISkin CreateLegacySkinProvider([NotNull] ISkin skin, IBeatmap beatmap) => null;
|
||||
|
||||
protected Ruleset()
|
||||
{
|
||||
|
@ -6,6 +6,7 @@ using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
@ -94,6 +95,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
/// Creates a <see cref="SelectionBlueprint{T}"/> for a specific item.
|
||||
/// </summary>
|
||||
/// <param name="item">The item to create the overlay for.</param>
|
||||
[CanBeNull]
|
||||
protected virtual SelectionBlueprint<T> CreateBlueprintFor(T item) => null;
|
||||
|
||||
protected virtual DragBox CreateDragBox(Action<RectangleF> performSelect) => new DragBox(performSelect);
|
||||
|
@ -4,6 +4,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Humanizer;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
@ -256,9 +257,10 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
if (drawable == null)
|
||||
return null;
|
||||
|
||||
return CreateHitObjectBlueprintFor(item).With(b => b.DrawableObject = drawable);
|
||||
return CreateHitObjectBlueprintFor(item)?.With(b => b.DrawableObject = drawable);
|
||||
}
|
||||
|
||||
[CanBeNull]
|
||||
public virtual HitObjectSelectionBlueprint CreateHitObjectBlueprintFor(HitObject hitObject) => null;
|
||||
|
||||
private void hitObjectAdded(HitObject obj)
|
||||
|
@ -7,6 +7,7 @@ using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@ -89,7 +90,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
}
|
||||
else
|
||||
{
|
||||
placementBlueprint = CreateBlueprintFor(obj.NewValue);
|
||||
placementBlueprint = CreateBlueprintFor(obj.NewValue).AsNonNull();
|
||||
|
||||
placementBlueprint.Colour = Color4.MediumPurple;
|
||||
|
||||
|
@ -73,15 +73,7 @@ namespace osu.Game.Screens.Edit.Compose
|
||||
{
|
||||
Debug.Assert(ruleset != null);
|
||||
|
||||
var beatmapSkinProvider = new BeatmapSkinProvidingContainer(beatmap.Value.Skin);
|
||||
|
||||
// the beatmapSkinProvider is used as the fallback source here to allow the ruleset-specific skin implementation
|
||||
// full access to all skin sources.
|
||||
var rulesetSkinProvider = new SkinProvidingContainer(ruleset.CreateLegacySkinProvider(beatmapSkinProvider, EditorBeatmap.PlayableBeatmap));
|
||||
|
||||
// load the skinning hierarchy first.
|
||||
// this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources.
|
||||
return beatmapSkinProvider.WithChild(rulesetSkinProvider.WithChild(content));
|
||||
return new RulesetSkinProvidingContainer(ruleset, EditorBeatmap.PlayableBeatmap, beatmap.Value.Skin).WithChild(content);
|
||||
}
|
||||
|
||||
#region Input Handling
|
||||
|
@ -305,18 +305,34 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.OnBackButton();
|
||||
}
|
||||
|
||||
public override bool OnExiting(IScreen next)
|
||||
{
|
||||
if (client.Room == null)
|
||||
{
|
||||
// room has not been created yet; exit immediately.
|
||||
return base.OnExiting(next);
|
||||
}
|
||||
|
||||
if (!exitConfirmed && dialogOverlay != null)
|
||||
{
|
||||
dialogOverlay.Push(new ConfirmDialog("Are you sure you want to leave this multiplayer match?", () =>
|
||||
if (dialogOverlay.CurrentDialog is ConfirmDialog confirmDialog)
|
||||
confirmDialog.PerformOkAction();
|
||||
else
|
||||
{
|
||||
exitConfirmed = true;
|
||||
this.Exit();
|
||||
}));
|
||||
dialogOverlay.Push(new ConfirmDialog("Are you sure you want to leave this multiplayer match?", () =>
|
||||
{
|
||||
exitConfirmed = true;
|
||||
this.Exit();
|
||||
}));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.OnBackButton();
|
||||
return base.OnExiting(next);
|
||||
}
|
||||
|
||||
private ModSettingChangeTracker modSettingChangeTracker;
|
||||
|
@ -240,13 +240,15 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
|
||||
public override bool OnExiting(IScreen next)
|
||||
{
|
||||
if (screenStack.CurrentScreen?.OnExiting(next) == true)
|
||||
return true;
|
||||
|
||||
RoomManager.PartRoom();
|
||||
|
||||
waves.Hide();
|
||||
|
||||
this.Delay(WaveContainer.DISAPPEAR_DURATION).FadeOut();
|
||||
|
||||
screenStack.CurrentScreen?.OnExiting(next);
|
||||
base.OnExiting(next);
|
||||
return false;
|
||||
}
|
||||
|
@ -228,29 +228,23 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
dependencies.CacheAs(GameplayBeatmap);
|
||||
|
||||
var beatmapSkinProvider = new BeatmapSkinProvidingContainer(Beatmap.Value.Skin);
|
||||
|
||||
// the beatmapSkinProvider is used as the fallback source here to allow the ruleset-specific skin implementation
|
||||
// full access to all skin sources.
|
||||
var rulesetSkinProvider = new SkinProvidingContainer(GameplayRuleset.CreateLegacySkinProvider(beatmapSkinProvider, playableBeatmap));
|
||||
var rulesetSkinProvider = new RulesetSkinProvidingContainer(GameplayRuleset, playableBeatmap, Beatmap.Value.Skin);
|
||||
|
||||
// load the skinning hierarchy first.
|
||||
// this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources.
|
||||
GameplayClockContainer.Add(beatmapSkinProvider.WithChild(rulesetSkinProvider));
|
||||
GameplayClockContainer.Add(rulesetSkinProvider);
|
||||
|
||||
rulesetSkinProvider.AddRange(new[]
|
||||
{
|
||||
// underlay and gameplay should have access the to skinning sources.
|
||||
// underlay and gameplay should have access to the skinning sources.
|
||||
createUnderlayComponents(),
|
||||
createGameplayComponents(Beatmap.Value, playableBeatmap)
|
||||
});
|
||||
|
||||
// also give the HUD a ruleset container to allow rulesets to potentially override HUD elements (used to disable combo counters etc.)
|
||||
// we may want to limit this in the future to disallow rulesets from outright replacing elements the user expects to be there.
|
||||
var hudRulesetContainer = new SkinProvidingContainer(GameplayRuleset.CreateLegacySkinProvider(beatmapSkinProvider, playableBeatmap));
|
||||
|
||||
// add the overlay components as a separate step as they proxy some elements from the above underlay/gameplay components.
|
||||
GameplayClockContainer.Add(hudRulesetContainer.WithChild(createOverlayComponents(Beatmap.Value)));
|
||||
// also give the overlays the ruleset skin provider to allow rulesets to potentially override HUD elements (used to disable combo counters etc.)
|
||||
// we may want to limit this in the future to disallow rulesets from outright replacing elements the user expects to be there.
|
||||
rulesetSkinProvider.Add(createOverlayComponents(Beatmap.Value));
|
||||
|
||||
if (!DrawableRuleset.AllowGameplayOverlays)
|
||||
{
|
||||
|
@ -83,9 +83,9 @@ namespace osu.Game.Skinning
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
beatmapSkins.BindValueChanged(_ => TriggerSourceChanged());
|
||||
beatmapColours.BindValueChanged(_ => TriggerSourceChanged());
|
||||
beatmapHitsounds.BindValueChanged(_ => TriggerSourceChanged());
|
||||
beatmapSkins.BindValueChanged(_ => OnSourceChanged());
|
||||
beatmapColours.BindValueChanged(_ => OnSourceChanged());
|
||||
beatmapHitsounds.BindValueChanged(_ => OnSourceChanged());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace osu.Game.Skinning
|
||||
@ -20,5 +21,10 @@ namespace osu.Game.Skinning
|
||||
/// <returns>The skin to be used for subsequent lookups, or <c>null</c> if none is available.</returns>
|
||||
[CanBeNull]
|
||||
ISkin FindProvider(Func<ISkin, bool> lookupFunction);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve all sources available for lookup, with highest priority source first.
|
||||
/// </summary>
|
||||
IEnumerable<ISkin> AllSources { get; }
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
@ -16,45 +17,38 @@ namespace osu.Game.Skinning
|
||||
/// <summary>
|
||||
/// Transformer used to handle support of legacy features for individual rulesets.
|
||||
/// </summary>
|
||||
public abstract class LegacySkinTransformer : ISkinSource
|
||||
public abstract class LegacySkinTransformer : ISkin
|
||||
{
|
||||
/// <summary>
|
||||
/// Source of the <see cref="ISkin"/> which is being transformed.
|
||||
/// The <see cref="ISkin"/> which is being transformed.
|
||||
/// </summary>
|
||||
protected ISkinSource Source { get; }
|
||||
[NotNull]
|
||||
protected ISkin Skin { get; }
|
||||
|
||||
protected LegacySkinTransformer(ISkinSource source)
|
||||
protected LegacySkinTransformer([NotNull] ISkin skin)
|
||||
{
|
||||
Source = source;
|
||||
Skin = skin ?? throw new ArgumentNullException(nameof(skin));
|
||||
}
|
||||
|
||||
public abstract Drawable GetDrawableComponent(ISkinComponent component);
|
||||
public virtual Drawable GetDrawableComponent(ISkinComponent component) => Skin.GetDrawableComponent(component);
|
||||
|
||||
public Texture GetTexture(string componentName) => GetTexture(componentName, default, default);
|
||||
|
||||
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
|
||||
=> Source.GetTexture(componentName, wrapModeS, wrapModeT);
|
||||
=> Skin.GetTexture(componentName, wrapModeS, wrapModeT);
|
||||
|
||||
public virtual ISample GetSample(ISampleInfo sampleInfo)
|
||||
{
|
||||
if (!(sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample))
|
||||
return Source.GetSample(sampleInfo);
|
||||
return Skin.GetSample(sampleInfo);
|
||||
|
||||
var playLayeredHitSounds = GetConfig<LegacySetting, bool>(LegacySetting.LayeredHitSounds);
|
||||
if (legacySample.IsLayered && playLayeredHitSounds?.Value == false)
|
||||
return new SampleVirtual();
|
||||
|
||||
return Source.GetSample(sampleInfo);
|
||||
return Skin.GetSample(sampleInfo);
|
||||
}
|
||||
|
||||
public abstract IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup);
|
||||
|
||||
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => Source.FindProvider(lookupFunction);
|
||||
|
||||
public event Action SourceChanged
|
||||
{
|
||||
add => Source.SourceChanged += value;
|
||||
remove => Source.SourceChanged -= value;
|
||||
}
|
||||
public virtual IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => Skin.GetConfig<TLookup, TValue>(lookup);
|
||||
}
|
||||
}
|
||||
|
100
osu.Game/Skinning/RulesetSkinProvidingContainer.cs
Normal file
100
osu.Game/Skinning/RulesetSkinProvidingContainer.cs
Normal file
@ -0,0 +1,100 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.UI;
|
||||
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
/// <summary>
|
||||
/// A type of <see cref="SkinProvidingContainer"/> specialized for <see cref="DrawableRuleset"/> and other gameplay-related components.
|
||||
/// Providing access to parent skin sources and the beatmap skin each surrounded with the ruleset legacy skin transformer.
|
||||
/// </summary>
|
||||
public class RulesetSkinProvidingContainer : SkinProvidingContainer
|
||||
{
|
||||
protected readonly Ruleset Ruleset;
|
||||
protected readonly IBeatmap Beatmap;
|
||||
|
||||
/// <remarks>
|
||||
/// This container already re-exposes all parent <see cref="ISkinSource"/> sources in a ruleset-usable form.
|
||||
/// Therefore disallow falling back to any parent <see cref="ISkinSource"/> any further.
|
||||
/// </remarks>
|
||||
protected override bool AllowFallingBackToParent => false;
|
||||
|
||||
protected override Container<Drawable> Content { get; }
|
||||
|
||||
public RulesetSkinProvidingContainer(Ruleset ruleset, IBeatmap beatmap, [CanBeNull] ISkin beatmapSkin)
|
||||
{
|
||||
Ruleset = ruleset;
|
||||
Beatmap = beatmap;
|
||||
|
||||
InternalChild = new BeatmapSkinProvidingContainer(beatmapSkin is LegacySkin ? GetLegacyRulesetTransformedSkin(beatmapSkin) : beatmapSkin)
|
||||
{
|
||||
Child = Content = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private ISkinSource skinSource { get; set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
UpdateSkins();
|
||||
skinSource.SourceChanged += OnSourceChanged;
|
||||
}
|
||||
|
||||
protected override void OnSourceChanged()
|
||||
{
|
||||
UpdateSkins();
|
||||
base.OnSourceChanged();
|
||||
}
|
||||
|
||||
protected virtual void UpdateSkins()
|
||||
{
|
||||
SkinSources.Clear();
|
||||
|
||||
foreach (var skin in skinSource.AllSources)
|
||||
{
|
||||
switch (skin)
|
||||
{
|
||||
case LegacySkin legacySkin:
|
||||
SkinSources.Add(GetLegacyRulesetTransformedSkin(legacySkin));
|
||||
break;
|
||||
|
||||
default:
|
||||
SkinSources.Add(skin);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected ISkin GetLegacyRulesetTransformedSkin(ISkin legacySkin)
|
||||
{
|
||||
if (legacySkin == null)
|
||||
return null;
|
||||
|
||||
var rulesetTransformed = Ruleset.CreateLegacySkinProvider(legacySkin, Beatmap);
|
||||
if (rulesetTransformed != null)
|
||||
return rulesetTransformed;
|
||||
|
||||
return legacySkin;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (skinSource != null)
|
||||
skinSource.SourceChanged -= OnSourceChanged;
|
||||
}
|
||||
}
|
||||
}
|
@ -30,6 +30,13 @@ using osu.Game.IO.Archives;
|
||||
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the storage and retrieval of <see cref="Skin"/>s.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is also exposed and cached as <see cref="ISkinSource"/> to allow for any component to potentially have skinning support.
|
||||
/// For gameplay components, see <see cref="RulesetSkinProvidingContainer"/> which adds extra legacy and toggle logic that may affect the lookup process.
|
||||
/// </remarks>
|
||||
[ExcludeFromDynamicCompile]
|
||||
public class SkinManager : ArchiveModelManager<SkinInfo, SkinFileInfo>, ISkinSource, IStorageResourceProvider
|
||||
{
|
||||
@ -48,9 +55,15 @@ namespace osu.Game.Skinning
|
||||
|
||||
protected override string ImportFromStablePath => "Skins";
|
||||
|
||||
private readonly Skin defaultLegacySkin;
|
||||
/// <summary>
|
||||
/// The default skin.
|
||||
/// </summary>
|
||||
public Skin DefaultSkin { get; }
|
||||
|
||||
private readonly Skin defaultSkin;
|
||||
/// <summary>
|
||||
/// The default legacy skin.
|
||||
/// </summary>
|
||||
public Skin DefaultLegacySkin { get; }
|
||||
|
||||
public SkinManager(Storage storage, DatabaseContextFactory contextFactory, GameHost host, IResourceStore<byte[]> resources, AudioManager audio)
|
||||
: base(storage, contextFactory, new SkinStore(contextFactory, storage), host)
|
||||
@ -59,12 +72,12 @@ namespace osu.Game.Skinning
|
||||
this.host = host;
|
||||
this.resources = resources;
|
||||
|
||||
defaultLegacySkin = new DefaultLegacySkin(this);
|
||||
defaultSkin = new DefaultSkin(this);
|
||||
DefaultLegacySkin = new DefaultLegacySkin(this);
|
||||
DefaultSkin = new DefaultSkin(this);
|
||||
|
||||
CurrentSkinInfo.ValueChanged += skin => CurrentSkin.Value = GetSkin(skin.NewValue);
|
||||
|
||||
CurrentSkin.Value = defaultSkin;
|
||||
CurrentSkin.Value = DefaultSkin;
|
||||
CurrentSkin.ValueChanged += skin =>
|
||||
{
|
||||
if (skin.NewValue.SkinInfo != CurrentSkinInfo.Value)
|
||||
@ -83,8 +96,8 @@ namespace osu.Game.Skinning
|
||||
public List<SkinInfo> GetAllUsableSkins()
|
||||
{
|
||||
var userSkins = GetAllUserSkins();
|
||||
userSkins.Insert(0, SkinInfo.Default);
|
||||
userSkins.Insert(1, DefaultLegacySkin.Info);
|
||||
userSkins.Insert(0, DefaultSkin.SkinInfo);
|
||||
userSkins.Insert(1, DefaultLegacySkin.SkinInfo);
|
||||
return userSkins;
|
||||
}
|
||||
|
||||
@ -223,32 +236,39 @@ namespace osu.Game.Skinning
|
||||
|
||||
public ISkin FindProvider(Func<ISkin, bool> lookupFunction)
|
||||
{
|
||||
if (lookupFunction(CurrentSkin.Value))
|
||||
return CurrentSkin.Value;
|
||||
|
||||
if (CurrentSkin.Value is LegacySkin && lookupFunction(defaultLegacySkin))
|
||||
return defaultLegacySkin;
|
||||
|
||||
if (lookupFunction(defaultSkin))
|
||||
return defaultSkin;
|
||||
foreach (var source in AllSources)
|
||||
{
|
||||
if (lookupFunction(source))
|
||||
return source;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public IEnumerable<ISkin> AllSources
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return CurrentSkin.Value;
|
||||
|
||||
if (CurrentSkin.Value is LegacySkin && CurrentSkin.Value != DefaultLegacySkin)
|
||||
yield return DefaultLegacySkin;
|
||||
|
||||
if (CurrentSkin.Value != DefaultSkin)
|
||||
yield return DefaultSkin;
|
||||
}
|
||||
}
|
||||
|
||||
private T lookupWithFallback<T>(Func<ISkin, T> lookupFunction)
|
||||
where T : class
|
||||
{
|
||||
if (lookupFunction(CurrentSkin.Value) is T skinSourced)
|
||||
return skinSourced;
|
||||
foreach (var source in AllSources)
|
||||
{
|
||||
if (lookupFunction(source) is T skinSourced)
|
||||
return skinSourced;
|
||||
}
|
||||
|
||||
// TODO: we also want to return a DefaultLegacySkin here if the current *beatmap* is providing any skinned elements.
|
||||
// When attempting to address this, we may want to move the full DefaultLegacySkin fallback logic to within Player itself (to better allow
|
||||
// for beatmap skin visibility).
|
||||
if (CurrentSkin.Value is LegacySkin && lookupFunction(defaultLegacySkin) is T legacySourced)
|
||||
return legacySourced;
|
||||
|
||||
// Finally fall back to the (non-legacy) default.
|
||||
return lookupFunction(defaultSkin);
|
||||
return null;
|
||||
}
|
||||
|
||||
#region IResourceStorageProvider
|
||||
|
@ -2,6 +2,9 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio.Sample;
|
||||
@ -21,13 +24,24 @@ namespace osu.Game.Skinning
|
||||
{
|
||||
public event Action SourceChanged;
|
||||
|
||||
[CanBeNull]
|
||||
private readonly ISkin skin;
|
||||
/// <summary>
|
||||
/// Skins which should be exposed by this container, in order of lookup precedence.
|
||||
/// </summary>
|
||||
protected readonly BindableList<ISkin> SkinSources = new BindableList<ISkin>();
|
||||
|
||||
/// <summary>
|
||||
/// A dictionary mapping each <see cref="ISkin"/> from the <see cref="SkinSources"/>
|
||||
/// to one that performs the "allow lookup" checks before proceeding with a lookup.
|
||||
/// </summary>
|
||||
private readonly Dictionary<ISkin, DisableableSkinSource> disableableSkinSources = new Dictionary<ISkin, DisableableSkinSource>();
|
||||
|
||||
[CanBeNull]
|
||||
private ISkinSource fallbackSource;
|
||||
|
||||
private readonly NoFallbackProxy noFallbackLookupProxy;
|
||||
/// <summary>
|
||||
/// Whether falling back to parent <see cref="ISkinSource"/>s is allowed in this container.
|
||||
/// </summary>
|
||||
protected virtual bool AllowFallingBackToParent => true;
|
||||
|
||||
protected virtual bool AllowDrawableLookup(ISkinComponent component) => true;
|
||||
|
||||
@ -39,123 +53,159 @@ namespace osu.Game.Skinning
|
||||
|
||||
protected virtual bool AllowColourLookup => true;
|
||||
|
||||
public SkinProvidingContainer(ISkin skin)
|
||||
/// <summary>
|
||||
/// Constructs a new <see cref="SkinProvidingContainer"/> initialised with a single skin source.
|
||||
/// </summary>
|
||||
public SkinProvidingContainer([CanBeNull] ISkin skin)
|
||||
: this()
|
||||
{
|
||||
this.skin = skin;
|
||||
if (skin != null)
|
||||
SkinSources.Add(skin);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new <see cref="SkinProvidingContainer"/> with no sources.
|
||||
/// Implementations can add or change sources through the <see cref="SkinSources"/> list.
|
||||
/// </summary>
|
||||
protected SkinProvidingContainer()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
noFallbackLookupProxy = new NoFallbackProxy(this);
|
||||
SkinSources.BindCollectionChanged(((_, args) =>
|
||||
{
|
||||
switch (args.Action)
|
||||
{
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
foreach (var skin in args.NewItems.Cast<ISkin>())
|
||||
{
|
||||
disableableSkinSources.Add(skin, new DisableableSkinSource(skin, this));
|
||||
|
||||
if (skin is ISkinSource source)
|
||||
source.SourceChanged += TriggerSourceChanged;
|
||||
if (skin is ISkinSource source)
|
||||
source.SourceChanged += OnSourceChanged;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case NotifyCollectionChangedAction.Reset:
|
||||
case NotifyCollectionChangedAction.Remove:
|
||||
foreach (var skin in args.OldItems.Cast<ISkin>())
|
||||
{
|
||||
disableableSkinSources.Remove(skin);
|
||||
|
||||
if (skin is ISkinSource source)
|
||||
source.SourceChanged -= OnSourceChanged;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case NotifyCollectionChangedAction.Replace:
|
||||
foreach (var skin in args.OldItems.Cast<ISkin>())
|
||||
{
|
||||
disableableSkinSources.Remove(skin);
|
||||
|
||||
if (skin is ISkinSource source)
|
||||
source.SourceChanged -= OnSourceChanged;
|
||||
}
|
||||
|
||||
foreach (var skin in args.NewItems.Cast<ISkin>())
|
||||
{
|
||||
disableableSkinSources.Add(skin, new DisableableSkinSource(skin, this));
|
||||
|
||||
if (skin is ISkinSource source)
|
||||
source.SourceChanged += OnSourceChanged;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}), true);
|
||||
}
|
||||
|
||||
public ISkin FindProvider(Func<ISkin, bool> lookupFunction)
|
||||
{
|
||||
if (skin is ISkinSource source)
|
||||
foreach (var skin in SkinSources)
|
||||
{
|
||||
if (source.FindProvider(lookupFunction) is ISkin found)
|
||||
return found;
|
||||
}
|
||||
else if (skin != null)
|
||||
{
|
||||
// a proxy must be used here to correctly pass through the "Allow" checks without implicitly falling back to the fallbackSource.
|
||||
if (lookupFunction(noFallbackLookupProxy))
|
||||
if (lookupFunction(disableableSkinSources[skin]))
|
||||
return skin;
|
||||
}
|
||||
|
||||
return fallbackSource?.FindProvider(lookupFunction);
|
||||
}
|
||||
|
||||
public Drawable GetDrawableComponent(ISkinComponent component)
|
||||
=> GetDrawableComponent(component, true);
|
||||
|
||||
public Drawable GetDrawableComponent(ISkinComponent component, bool fallback)
|
||||
public IEnumerable<ISkin> AllSources
|
||||
{
|
||||
Drawable sourceDrawable;
|
||||
if (AllowDrawableLookup(component) && (sourceDrawable = skin?.GetDrawableComponent(component)) != null)
|
||||
return sourceDrawable;
|
||||
get
|
||||
{
|
||||
foreach (var skin in SkinSources)
|
||||
yield return skin;
|
||||
|
||||
if (!fallback)
|
||||
return null;
|
||||
if (fallbackSource != null)
|
||||
{
|
||||
foreach (var skin in fallbackSource.AllSources)
|
||||
yield return skin;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Drawable GetDrawableComponent(ISkinComponent component)
|
||||
{
|
||||
foreach (var skin in SkinSources)
|
||||
{
|
||||
Drawable sourceDrawable;
|
||||
if ((sourceDrawable = disableableSkinSources[skin]?.GetDrawableComponent(component)) != null)
|
||||
return sourceDrawable;
|
||||
}
|
||||
|
||||
return fallbackSource?.GetDrawableComponent(component);
|
||||
}
|
||||
|
||||
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
|
||||
=> GetTexture(componentName, wrapModeS, wrapModeT, true);
|
||||
|
||||
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT, bool fallback)
|
||||
{
|
||||
Texture sourceTexture;
|
||||
if (AllowTextureLookup(componentName) && (sourceTexture = skin?.GetTexture(componentName, wrapModeS, wrapModeT)) != null)
|
||||
return sourceTexture;
|
||||
|
||||
if (!fallback)
|
||||
return null;
|
||||
foreach (var skin in SkinSources)
|
||||
{
|
||||
Texture sourceTexture;
|
||||
if ((sourceTexture = disableableSkinSources[skin]?.GetTexture(componentName, wrapModeS, wrapModeT)) != null)
|
||||
return sourceTexture;
|
||||
}
|
||||
|
||||
return fallbackSource?.GetTexture(componentName, wrapModeS, wrapModeT);
|
||||
}
|
||||
|
||||
public ISample GetSample(ISampleInfo sampleInfo)
|
||||
=> GetSample(sampleInfo, true);
|
||||
|
||||
public ISample GetSample(ISampleInfo sampleInfo, bool fallback)
|
||||
{
|
||||
ISample sourceChannel;
|
||||
if (AllowSampleLookup(sampleInfo) && (sourceChannel = skin?.GetSample(sampleInfo)) != null)
|
||||
return sourceChannel;
|
||||
|
||||
if (!fallback)
|
||||
return null;
|
||||
foreach (var skin in SkinSources)
|
||||
{
|
||||
ISample sourceSample;
|
||||
if ((sourceSample = disableableSkinSources[skin]?.GetSample(sampleInfo)) != null)
|
||||
return sourceSample;
|
||||
}
|
||||
|
||||
return fallbackSource?.GetSample(sampleInfo);
|
||||
}
|
||||
|
||||
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
|
||||
=> GetConfig<TLookup, TValue>(lookup, true);
|
||||
|
||||
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup, bool fallback)
|
||||
{
|
||||
if (skin != null)
|
||||
foreach (var skin in SkinSources)
|
||||
{
|
||||
if (lookup is GlobalSkinColours || lookup is SkinCustomColourLookup)
|
||||
return lookupWithFallback<TLookup, TValue>(lookup, AllowColourLookup, fallback);
|
||||
|
||||
return lookupWithFallback<TLookup, TValue>(lookup, AllowConfigurationLookup, fallback);
|
||||
}
|
||||
|
||||
if (!fallback)
|
||||
return null;
|
||||
|
||||
return fallbackSource?.GetConfig<TLookup, TValue>(lookup);
|
||||
}
|
||||
|
||||
private IBindable<TValue> lookupWithFallback<TLookup, TValue>(TLookup lookup, bool canUseSkinLookup, bool canUseFallback)
|
||||
{
|
||||
if (canUseSkinLookup)
|
||||
{
|
||||
var bindable = skin?.GetConfig<TLookup, TValue>(lookup);
|
||||
if (bindable != null)
|
||||
IBindable<TValue> bindable;
|
||||
if ((bindable = disableableSkinSources[skin]?.GetConfig<TLookup, TValue>(lookup)) != null)
|
||||
return bindable;
|
||||
}
|
||||
|
||||
if (!canUseFallback)
|
||||
return null;
|
||||
|
||||
return fallbackSource?.GetConfig<TLookup, TValue>(lookup);
|
||||
}
|
||||
|
||||
protected virtual void TriggerSourceChanged() => SourceChanged?.Invoke();
|
||||
protected virtual void OnSourceChanged() => SourceChanged?.Invoke();
|
||||
|
||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
||||
{
|
||||
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
|
||||
|
||||
fallbackSource = dependencies.Get<ISkinSource>();
|
||||
if (fallbackSource != null)
|
||||
fallbackSource.SourceChanged += TriggerSourceChanged;
|
||||
if (AllowFallingBackToParent)
|
||||
{
|
||||
fallbackSource = dependencies.Get<ISkinSource>();
|
||||
if (fallbackSource != null)
|
||||
fallbackSource.SourceChanged += OnSourceChanged;
|
||||
}
|
||||
|
||||
dependencies.CacheAs<ISkinSource>(this);
|
||||
|
||||
@ -170,41 +220,67 @@ namespace osu.Game.Skinning
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (fallbackSource != null)
|
||||
fallbackSource.SourceChanged -= TriggerSourceChanged;
|
||||
fallbackSource.SourceChanged -= OnSourceChanged;
|
||||
|
||||
if (skin is ISkinSource source)
|
||||
source.SourceChanged -= TriggerSourceChanged;
|
||||
foreach (var source in SkinSources.OfType<ISkinSource>())
|
||||
source.SourceChanged -= OnSourceChanged;
|
||||
}
|
||||
|
||||
private class NoFallbackProxy : ISkinSource
|
||||
private class DisableableSkinSource : ISkin
|
||||
{
|
||||
private readonly ISkin skin;
|
||||
private readonly SkinProvidingContainer provider;
|
||||
|
||||
public NoFallbackProxy(SkinProvidingContainer provider)
|
||||
public DisableableSkinSource(ISkin skin, SkinProvidingContainer provider)
|
||||
{
|
||||
this.skin = skin;
|
||||
this.provider = provider;
|
||||
}
|
||||
|
||||
public Drawable GetDrawableComponent(ISkinComponent component)
|
||||
=> provider.GetDrawableComponent(component, false);
|
||||
|
||||
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
|
||||
=> provider.GetTexture(componentName, wrapModeS, wrapModeT, false);
|
||||
|
||||
public ISample GetSample(ISampleInfo sampleInfo)
|
||||
=> provider.GetSample(sampleInfo, false);
|
||||
|
||||
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
|
||||
=> provider.GetConfig<TLookup, TValue>(lookup, false);
|
||||
|
||||
public event Action SourceChanged
|
||||
{
|
||||
add => provider.SourceChanged += value;
|
||||
remove => provider.SourceChanged -= value;
|
||||
if (provider.AllowDrawableLookup(component))
|
||||
return skin.GetDrawableComponent(component);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) =>
|
||||
provider.FindProvider(lookupFunction);
|
||||
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
|
||||
{
|
||||
if (provider.AllowTextureLookup(componentName))
|
||||
return skin.GetTexture(componentName, wrapModeS, wrapModeT);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public ISample GetSample(ISampleInfo sampleInfo)
|
||||
{
|
||||
if (provider.AllowSampleLookup(sampleInfo))
|
||||
return skin.GetSample(sampleInfo);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
|
||||
{
|
||||
switch (lookup)
|
||||
{
|
||||
case GlobalSkinColours _:
|
||||
case SkinCustomColourLookup _:
|
||||
if (provider.AllowColourLookup)
|
||||
return skin.GetConfig<TLookup, TValue>(lookup);
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
if (provider.AllowConfigurationLookup)
|
||||
return skin.GetConfig<TLookup, TValue>(lookup);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -159,7 +159,9 @@ namespace osu.Game.Tests.Beatmaps
|
||||
remove { }
|
||||
}
|
||||
|
||||
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => null;
|
||||
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => lookupFunction(this) ? this : null;
|
||||
|
||||
public IEnumerable<ISkin> AllSources => new[] { this };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +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.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Screens;
|
||||
|
||||
namespace osu.Game.Tests.Visual
|
||||
@ -19,12 +21,16 @@ namespace osu.Game.Tests.Visual
|
||||
|
||||
protected override Container<Drawable> Content => content;
|
||||
|
||||
[Cached]
|
||||
protected DialogOverlay DialogOverlay { get; private set; }
|
||||
|
||||
protected ScreenTestScene()
|
||||
{
|
||||
base.Content.AddRange(new Drawable[]
|
||||
{
|
||||
Stack = new OsuScreenStack { RelativeSizeAxes = Axes.Both },
|
||||
content = new Container { RelativeSizeAxes = Axes.Both }
|
||||
content = new Container { RelativeSizeAxes = Axes.Both },
|
||||
DialogOverlay = new DialogOverlay()
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -51,14 +51,31 @@ namespace osu.Game.Utils
|
||||
/// <returns>Whether all <see cref="Mod"/>s in the combination are compatible with each-other.</returns>
|
||||
public static bool CheckCompatibleSet(IEnumerable<Mod> combination, [NotNullWhen(false)] out List<Mod>? invalidMods)
|
||||
{
|
||||
combination = FlattenMods(combination).ToArray();
|
||||
var mods = FlattenMods(combination).ToArray();
|
||||
invalidMods = null;
|
||||
|
||||
foreach (var mod in combination)
|
||||
// ensure there are no duplicate mod definitions.
|
||||
for (int i = 0; i < mods.Length; i++)
|
||||
{
|
||||
var candidate = mods[i];
|
||||
|
||||
for (int j = i + 1; j < mods.Length; j++)
|
||||
{
|
||||
var m = mods[j];
|
||||
|
||||
if (candidate.Equals(m))
|
||||
{
|
||||
invalidMods ??= new List<Mod>();
|
||||
invalidMods.Add(m);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var mod in mods)
|
||||
{
|
||||
foreach (var type in mod.IncompatibleMods)
|
||||
{
|
||||
foreach (var invalid in combination.Where(m => type.IsInstanceOfType(m)))
|
||||
foreach (var invalid in mods.Where(m => type.IsInstanceOfType(m)))
|
||||
{
|
||||
if (invalid == mod)
|
||||
continue;
|
||||
|
Loading…
Reference in New Issue
Block a user