mirror of
https://github.com/ppy/osu
synced 2025-03-06 03:19:47 +00:00
Merge branch 'master' into transformers-per-skin
This commit is contained in:
commit
1b0aadcc6f
3
.gitignore
vendored
3
.gitignore
vendored
@ -336,3 +336,6 @@ inspectcode
|
||||
/BenchmarkDotNet.Artifacts
|
||||
|
||||
*.GeneratedMSBuildEditorConfig.editorconfig
|
||||
|
||||
# Fody (pulled in by Realm) - schema file
|
||||
FodyWeavers.xsd
|
||||
|
3
FodyWeavers.xml
Normal file
3
FodyWeavers.xml
Normal file
@ -0,0 +1,3 @@
|
||||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
|
||||
<Realm />
|
||||
</Weavers>
|
@ -43,7 +43,7 @@ If your platform is not listed above, there is still a chance you can manually b
|
||||
|
||||
osu! is designed to have extensible modular gameplay modes, called "rulesets". Building one of these allows a developer to harness the power of osu! for their own game style. To get started working on a ruleset, we have some templates available [here](https://github.com/ppy/osu/tree/master/Templates).
|
||||
|
||||
You can see some examples of custom rulesets by visiting the [custom ruleset directory](https://github.com/ppy/osu/issues/5852).
|
||||
You can see some examples of custom rulesets by visiting the [custom ruleset directory](https://github.com/ppy/osu/discussions/13096).
|
||||
|
||||
## Developing osu!
|
||||
|
||||
|
@ -51,7 +51,11 @@
|
||||
<Reference Include="Java.Interop" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.614.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.614.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.618.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.616.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Transitive Dependencies">
|
||||
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
|
||||
<PackageReference Include="Realm" Version="10.2.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -174,8 +174,8 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
|
||||
private void addToPlayfield(DrawableCatchHitObject drawable)
|
||||
{
|
||||
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObjects>())
|
||||
mod.ApplyToDrawableHitObjects(new[] { drawable });
|
||||
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObject>())
|
||||
mod.ApplyToDrawableHitObject(drawable);
|
||||
|
||||
drawableRuleset.Playfield.Add(drawable);
|
||||
}
|
||||
|
@ -12,12 +12,10 @@ using osu.Game.Rulesets.Catch.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Skinning.Default
|
||||
{
|
||||
public class DefaultCatcher : CompositeDrawable, ICatcherSprite
|
||||
public class DefaultCatcher : CompositeDrawable
|
||||
{
|
||||
public Bindable<CatcherAnimationState> CurrentState { get; } = new Bindable<CatcherAnimationState>();
|
||||
|
||||
public Texture CurrentTexture => sprite.Texture;
|
||||
|
||||
private readonly Sprite sprite;
|
||||
|
||||
private readonly Dictionary<CatcherAnimationState, Texture> textures = new Dictionary<CatcherAnimationState, Texture>();
|
||||
|
@ -1,12 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics.Textures;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Skinning
|
||||
{
|
||||
public interface ICatcherSprite
|
||||
{
|
||||
Texture CurrentTexture { get; }
|
||||
}
|
||||
}
|
@ -9,21 +9,17 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Animations;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
{
|
||||
public class LegacyCatcherNew : CompositeDrawable, ICatcherSprite
|
||||
public class LegacyCatcherNew : CompositeDrawable
|
||||
{
|
||||
[Resolved]
|
||||
private Bindable<CatcherAnimationState> currentState { get; set; }
|
||||
|
||||
public Texture CurrentTexture => (currentDrawable as TextureAnimation)?.CurrentFrame ?? (currentDrawable as Sprite)?.Texture;
|
||||
|
||||
private readonly Dictionary<CatcherAnimationState, Drawable> drawables = new Dictionary<CatcherAnimationState, Drawable>();
|
||||
|
||||
private Drawable currentDrawable;
|
||||
|
@ -3,19 +3,14 @@
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Animations;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
{
|
||||
public class LegacyCatcherOld : CompositeDrawable, ICatcherSprite
|
||||
public class LegacyCatcherOld : CompositeDrawable
|
||||
{
|
||||
public Texture CurrentTexture => (InternalChild as TextureAnimation)?.CurrentFrame ?? (InternalChild as Sprite)?.Texture;
|
||||
|
||||
public LegacyCatcherOld()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
@ -9,7 +9,6 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
@ -17,7 +16,6 @@ using osu.Game.Rulesets.Catch.Judgements;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Catch.Skinning;
|
||||
using osu.Game.Rulesets.Catch.Skinning.Default;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
@ -83,18 +81,17 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
/// </summary>
|
||||
private readonly Container<CaughtObject> droppedObjectTarget;
|
||||
|
||||
[Cached]
|
||||
protected readonly Bindable<CatcherAnimationState> CurrentStateBindable = new Bindable<CatcherAnimationState>();
|
||||
|
||||
public CatcherAnimationState CurrentState => CurrentStateBindable.Value;
|
||||
public CatcherAnimationState CurrentState
|
||||
{
|
||||
get => body.AnimationState.Value;
|
||||
private set => body.AnimationState.Value = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The width of the catcher which can receive fruit. Equivalent to "catchMargin" in osu-stable.
|
||||
/// </summary>
|
||||
public const float ALLOWED_CATCH_RANGE = 0.8f;
|
||||
|
||||
internal Texture CurrentTexture => ((ICatcherSprite)currentCatcher.Drawable).CurrentTexture;
|
||||
|
||||
private bool dashing;
|
||||
|
||||
public bool Dashing
|
||||
@ -121,7 +118,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
/// </summary>
|
||||
private readonly float catchWidth;
|
||||
|
||||
private readonly SkinnableDrawable currentCatcher;
|
||||
private readonly SkinnableCatcher body;
|
||||
|
||||
private Color4 hyperDashColour = DEFAULT_HYPER_DASH_COLOUR;
|
||||
private Color4 hyperDashEndGlowColour = DEFAULT_HYPER_DASH_COLOUR;
|
||||
@ -161,13 +158,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
},
|
||||
currentCatcher = new SkinnableDrawable(
|
||||
new CatchSkinComponent(CatchSkinComponents.Catcher),
|
||||
_ => new DefaultCatcher())
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
OriginPosition = new Vector2(0.5f, 0.06f) * CatcherArea.CATCHER_SIZE
|
||||
},
|
||||
body = new SkinnableCatcher(),
|
||||
hitExplosionContainer = new HitExplosionContainer
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
@ -268,17 +259,16 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
SetHyperDashState();
|
||||
|
||||
if (result.IsHit)
|
||||
updateState(hitObject.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle);
|
||||
CurrentState = hitObject.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle;
|
||||
else if (!(hitObject is Banana))
|
||||
updateState(CatcherAnimationState.Fail);
|
||||
CurrentState = CatcherAnimationState.Fail;
|
||||
}
|
||||
|
||||
public void OnRevertResult(DrawableCatchHitObject drawableObject, JudgementResult result)
|
||||
{
|
||||
var catchResult = (CatchJudgementResult)result;
|
||||
|
||||
if (CurrentState != catchResult.CatcherAnimationState)
|
||||
updateState(catchResult.CatcherAnimationState);
|
||||
CurrentState = catchResult.CatcherAnimationState;
|
||||
|
||||
if (HyperDashing != catchResult.CatcherHyperDash)
|
||||
{
|
||||
@ -373,14 +363,6 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
}
|
||||
}
|
||||
|
||||
private void updateState(CatcherAnimationState state)
|
||||
{
|
||||
if (CurrentState == state)
|
||||
return;
|
||||
|
||||
CurrentStateBindable.Value = state;
|
||||
}
|
||||
|
||||
private void placeCaughtObject(DrawablePalpableCatchHitObject drawableObject, Vector2 position)
|
||||
{
|
||||
var caughtObject = getCaughtObject(drawableObject.HitObject);
|
||||
|
43
osu.Game.Rulesets.Catch/UI/CatcherTrail.cs
Normal file
43
osu.Game.Rulesets.Catch/UI/CatcherTrail.cs
Normal file
@ -0,0 +1,43 @@
|
||||
// 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.Framework.Graphics.Pooling;
|
||||
using osu.Framework.Timing;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// A trail of the catcher.
|
||||
/// It also represents a hyper dash afterimage.
|
||||
/// </summary>
|
||||
public class CatcherTrail : PoolableDrawable
|
||||
{
|
||||
public CatcherAnimationState AnimationState
|
||||
{
|
||||
set => body.AnimationState.Value = value;
|
||||
}
|
||||
|
||||
private readonly SkinnableCatcher body;
|
||||
|
||||
public CatcherTrail()
|
||||
{
|
||||
Size = new Vector2(CatcherArea.CATCHER_SIZE);
|
||||
Origin = Anchor.TopCentre;
|
||||
Blending = BlendingParameters.Additive;
|
||||
InternalChild = body = new SkinnableCatcher
|
||||
{
|
||||
// Using a frozen clock because trails should not be animated when the skin has an animated catcher.
|
||||
// TODO: The animation should be frozen at the animation frame at the time of the trail generation.
|
||||
Clock = new FramedClock(new ManualClock()),
|
||||
};
|
||||
}
|
||||
|
||||
protected override void FreeAfterUse()
|
||||
{
|
||||
ClearTransforms();
|
||||
base.FreeAfterUse();
|
||||
}
|
||||
}
|
||||
}
|
@ -19,11 +19,11 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
{
|
||||
private readonly Catcher catcher;
|
||||
|
||||
private readonly DrawablePool<CatcherTrailSprite> trailPool;
|
||||
private readonly DrawablePool<CatcherTrail> trailPool;
|
||||
|
||||
private readonly Container<CatcherTrailSprite> dashTrails;
|
||||
private readonly Container<CatcherTrailSprite> hyperDashTrails;
|
||||
private readonly Container<CatcherTrailSprite> endGlowSprites;
|
||||
private readonly Container<CatcherTrail> dashTrails;
|
||||
private readonly Container<CatcherTrail> hyperDashTrails;
|
||||
private readonly Container<CatcherTrail> endGlowSprites;
|
||||
|
||||
private Color4 hyperDashTrailsColour = Catcher.DEFAULT_HYPER_DASH_COLOUR;
|
||||
|
||||
@ -83,10 +83,10 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
trailPool = new DrawablePool<CatcherTrailSprite>(30),
|
||||
dashTrails = new Container<CatcherTrailSprite> { RelativeSizeAxes = Axes.Both },
|
||||
hyperDashTrails = new Container<CatcherTrailSprite> { RelativeSizeAxes = Axes.Both, Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR },
|
||||
endGlowSprites = new Container<CatcherTrailSprite> { RelativeSizeAxes = Axes.Both, Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR },
|
||||
trailPool = new DrawablePool<CatcherTrail>(30),
|
||||
dashTrails = new Container<CatcherTrail> { RelativeSizeAxes = Axes.Both },
|
||||
hyperDashTrails = new Container<CatcherTrail> { RelativeSizeAxes = Axes.Both, Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR },
|
||||
endGlowSprites = new Container<CatcherTrail> { RelativeSizeAxes = Axes.Both, Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR },
|
||||
};
|
||||
}
|
||||
|
||||
@ -116,15 +116,12 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
Scheduler.AddDelayed(displayTrail, catcher.HyperDashing ? 25 : 50);
|
||||
}
|
||||
|
||||
private CatcherTrailSprite createTrailSprite(Container<CatcherTrailSprite> target)
|
||||
private CatcherTrail createTrailSprite(Container<CatcherTrail> target)
|
||||
{
|
||||
CatcherTrailSprite sprite = trailPool.Get();
|
||||
CatcherTrail sprite = trailPool.Get();
|
||||
|
||||
sprite.Texture = catcher.CurrentTexture;
|
||||
sprite.Anchor = catcher.Anchor;
|
||||
sprite.AnimationState = catcher.CurrentState;
|
||||
sprite.Scale = catcher.Scale;
|
||||
sprite.Blending = BlendingParameters.Additive;
|
||||
sprite.RelativePositionAxes = catcher.RelativePositionAxes;
|
||||
sprite.Position = catcher.Position;
|
||||
|
||||
target.Add(sprite);
|
||||
|
@ -1,40 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.UI
|
||||
{
|
||||
public class CatcherTrailSprite : PoolableDrawable
|
||||
{
|
||||
public Texture Texture
|
||||
{
|
||||
set => sprite.Texture = value;
|
||||
}
|
||||
|
||||
private readonly Sprite sprite;
|
||||
|
||||
public CatcherTrailSprite()
|
||||
{
|
||||
InternalChild = sprite = new Sprite
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
};
|
||||
|
||||
Size = new Vector2(CatcherArea.CATCHER_SIZE);
|
||||
|
||||
// Sets the origin roughly to the centre of the catcher's plate to allow for correct scaling.
|
||||
OriginPosition = new Vector2(0.5f, 0.06f) * CatcherArea.CATCHER_SIZE;
|
||||
}
|
||||
|
||||
protected override void FreeAfterUse()
|
||||
{
|
||||
ClearTransforms();
|
||||
base.FreeAfterUse();
|
||||
}
|
||||
}
|
||||
}
|
33
osu.Game.Rulesets.Catch/UI/SkinnableCatcher.cs
Normal file
33
osu.Game.Rulesets.Catch/UI/SkinnableCatcher.cs
Normal file
@ -0,0 +1,33 @@
|
||||
// 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.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Catch.Skinning.Default;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// The visual representation of the <see cref="Catcher"/>.
|
||||
/// It includes the body part of the catcher and the catcher plate.
|
||||
/// </summary>
|
||||
public class SkinnableCatcher : SkinnableDrawable
|
||||
{
|
||||
/// <summary>
|
||||
/// This is used by skin elements to determine which texture of the catcher is used.
|
||||
/// </summary>
|
||||
[Cached]
|
||||
public readonly Bindable<CatcherAnimationState> AnimationState = new Bindable<CatcherAnimationState>();
|
||||
|
||||
public SkinnableCatcher()
|
||||
: base(new CatchSkinComponent(CatchSkinComponents.Catcher), _ => new DefaultCatcher())
|
||||
{
|
||||
Anchor = Anchor.TopCentre;
|
||||
// Sets the origin roughly to the centre of the catcher's plate to allow for correct scaling.
|
||||
OriginPosition = new Vector2(0.5f, 0.06f) * CatcherArea.CATCHER_SIZE;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,31 +1,54 @@
|
||||
// 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.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
{
|
||||
public abstract class ManiaSelectionBlueprintTestScene : SelectionBlueprintTestScene
|
||||
{
|
||||
[Cached(Type = typeof(IAdjustableClock))]
|
||||
private readonly IAdjustableClock clock = new StopwatchClock();
|
||||
protected override Container<Drawable> Content => blueprints ?? base.Content;
|
||||
|
||||
protected ManiaSelectionBlueprintTestScene()
|
||||
private readonly Container blueprints;
|
||||
|
||||
[Cached(typeof(Playfield))]
|
||||
public Playfield Playfield { get; }
|
||||
|
||||
private readonly ScrollingTestContainer scrollingTestContainer;
|
||||
|
||||
protected ScrollingDirection Direction
|
||||
{
|
||||
Add(new Column(0)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
AccentColour = Color4.OrangeRed,
|
||||
Clock = new FramedClock(new StopwatchClock()), // No scroll
|
||||
});
|
||||
set => scrollingTestContainer.Direction = value;
|
||||
}
|
||||
|
||||
public ManiaPlayfield Playfield => null;
|
||||
protected ManiaSelectionBlueprintTestScene(int columns)
|
||||
{
|
||||
var stageDefinitions = new List<StageDefinition> { new StageDefinition { Columns = columns } };
|
||||
base.Content.Child = scrollingTestContainer = new ScrollingTestContainer(ScrollingDirection.Up)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Playfield = new ManiaPlayfield(stageDefinitions)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
blueprints = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AddToggleStep("Downward scroll", b => Direction = b ? ScrollingDirection.Down : ScrollingDirection.Up);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,55 +1,32 @@
|
||||
// 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.Framework.Graphics.Containers;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Mania.Edit.Blueprints;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Objects.Drawables;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
{
|
||||
public class TestSceneHoldNoteSelectionBlueprint : ManiaSelectionBlueprintTestScene
|
||||
{
|
||||
private readonly DrawableHoldNote drawableObject;
|
||||
|
||||
protected override Container<Drawable> Content => content ?? base.Content;
|
||||
private readonly Container content;
|
||||
|
||||
public TestSceneHoldNoteSelectionBlueprint()
|
||||
: base(4)
|
||||
{
|
||||
var holdNote = new HoldNote { Column = 0, Duration = 1000 };
|
||||
holdNote.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
|
||||
base.Content.Child = content = new ScrollingTestContainer(ScrollingDirection.Down)
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Width = 50,
|
||||
Child = drawableObject = new DrawableHoldNote(holdNote)
|
||||
var holdNote = new HoldNote
|
||||
{
|
||||
Height = 300,
|
||||
AccentColour = { Value = OsuColour.Gray(0.3f) }
|
||||
}
|
||||
};
|
||||
Column = i,
|
||||
StartTime = i * 100,
|
||||
Duration = 500
|
||||
};
|
||||
holdNote.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
|
||||
AddBlueprint(new HoldNoteSelectionBlueprint(holdNote), drawableObject);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
foreach (var nested in drawableObject.NestedHitObjects)
|
||||
{
|
||||
double finalPosition = (nested.HitObject.StartTime - drawableObject.HitObject.StartTime) / drawableObject.HitObject.Duration;
|
||||
nested.Y = (float)(-finalPosition * content.DrawHeight);
|
||||
var drawableHitObject = new DrawableHoldNote(holdNote);
|
||||
Playfield.Add(drawableHitObject);
|
||||
AddBlueprint(new HoldNoteSelectionBlueprint(holdNote), drawableHitObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Edit;
|
||||
using osu.Game.Rulesets.Mania.Edit.Blueprints;
|
||||
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Mania.Skinning.Default;
|
||||
@ -184,8 +184,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
AddAssert("head note positioned correctly", () => Precision.AlmostEquals(holdNote.ScreenSpaceDrawQuad.BottomLeft, holdNote.Head.ScreenSpaceDrawQuad.BottomLeft));
|
||||
AddAssert("tail note positioned correctly", () => Precision.AlmostEquals(holdNote.ScreenSpaceDrawQuad.TopLeft, holdNote.Tail.ScreenSpaceDrawQuad.BottomLeft));
|
||||
|
||||
AddAssert("head blueprint positioned correctly", () => this.ChildrenOfType<HoldNoteNoteOverlay>().ElementAt(0).DrawPosition == holdNote.Head.DrawPosition);
|
||||
AddAssert("tail blueprint positioned correctly", () => this.ChildrenOfType<HoldNoteNoteOverlay>().ElementAt(1).DrawPosition == holdNote.Tail.DrawPosition);
|
||||
AddAssert("head blueprint positioned correctly", () => this.ChildrenOfType<EditNotePiece>().ElementAt(0).DrawPosition == holdNote.Head.DrawPosition);
|
||||
AddAssert("tail blueprint positioned correctly", () => this.ChildrenOfType<EditNotePiece>().ElementAt(1).DrawPosition == holdNote.Tail.DrawPosition);
|
||||
}
|
||||
|
||||
private void setScrollStep(ScrollingDirection direction)
|
||||
|
@ -1,40 +1,32 @@
|
||||
// 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.Framework.Graphics.Containers;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Mania.Edit.Blueprints;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Objects.Drawables;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
{
|
||||
public class TestSceneNoteSelectionBlueprint : ManiaSelectionBlueprintTestScene
|
||||
{
|
||||
protected override Container<Drawable> Content => content ?? base.Content;
|
||||
private readonly Container content;
|
||||
|
||||
public TestSceneNoteSelectionBlueprint()
|
||||
: base(4)
|
||||
{
|
||||
var note = new Note { Column = 0 };
|
||||
note.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
|
||||
DrawableNote drawableObject;
|
||||
|
||||
base.Content.Child = content = new ScrollingTestContainer(ScrollingDirection.Down)
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(50, 20),
|
||||
Child = drawableObject = new DrawableNote(note)
|
||||
};
|
||||
var note = new Note
|
||||
{
|
||||
Column = i,
|
||||
StartTime = i * 200,
|
||||
};
|
||||
note.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
|
||||
AddBlueprint(new NoteSelectionBlueprint(note), drawableObject);
|
||||
var drawableHitObject = new DrawableNote(note);
|
||||
Playfield.Add(drawableHitObject);
|
||||
AddBlueprint(new NoteSelectionBlueprint(note), drawableHitObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,43 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
|
||||
using osu.Game.Rulesets.Mania.Objects.Drawables;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
{
|
||||
public class HoldNoteNoteOverlay : CompositeDrawable
|
||||
{
|
||||
private readonly HoldNoteSelectionBlueprint holdNoteBlueprint;
|
||||
private readonly HoldNotePosition position;
|
||||
|
||||
public HoldNoteNoteOverlay(HoldNoteSelectionBlueprint holdNoteBlueprint, HoldNotePosition position)
|
||||
{
|
||||
this.holdNoteBlueprint = holdNoteBlueprint;
|
||||
this.position = position;
|
||||
|
||||
InternalChild = new EditNotePiece { RelativeSizeAxes = Axes.X };
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
var drawableObject = holdNoteBlueprint.DrawableObject;
|
||||
|
||||
// Todo: This shouldn't exist, mania should not reference the drawable hitobject directly.
|
||||
if (drawableObject.IsLoaded)
|
||||
{
|
||||
DrawableNote note = position == HoldNotePosition.Start ? (DrawableNote)drawableObject.Head : drawableObject.Tail;
|
||||
|
||||
Anchor = note.Anchor;
|
||||
Origin = note.Origin;
|
||||
|
||||
Size = note.DrawSize;
|
||||
Position = note.DrawPosition;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
{
|
||||
public enum HoldNotePosition
|
||||
{
|
||||
Start,
|
||||
End
|
||||
}
|
||||
}
|
@ -2,14 +2,13 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Objects.Drawables;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osuTK;
|
||||
|
||||
@ -17,13 +16,12 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
{
|
||||
public class HoldNoteSelectionBlueprint : ManiaSelectionBlueprint<HoldNote>
|
||||
{
|
||||
public new DrawableHoldNote DrawableObject => (DrawableHoldNote)base.DrawableObject;
|
||||
|
||||
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; }
|
||||
|
||||
private EditNotePiece head;
|
||||
private EditNotePiece tail;
|
||||
|
||||
public HoldNoteSelectionBlueprint(HoldNote hold)
|
||||
: base(hold)
|
||||
{
|
||||
@ -32,12 +30,10 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(IScrollingInfo scrollingInfo)
|
||||
{
|
||||
direction.BindTo(scrollingInfo.Direction);
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new HoldNoteNoteOverlay(this, HoldNotePosition.Start),
|
||||
new HoldNoteNoteOverlay(this, HoldNotePosition.End),
|
||||
head = new EditNotePiece { RelativeSizeAxes = Axes.X },
|
||||
tail = new EditNotePiece { RelativeSizeAxes = Axes.X },
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
@ -58,21 +54,13 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
{
|
||||
base.Update();
|
||||
|
||||
// Todo: This shouldn't exist, mania should not reference the drawable hitobject directly.
|
||||
if (DrawableObject.IsLoaded)
|
||||
{
|
||||
Size = DrawableObject.DrawSize + new Vector2(0, DrawableObject.Tail.DrawHeight);
|
||||
|
||||
// This is a side-effect of not matching the hitobject's anchors/origins, which is kinda hard to do
|
||||
// When scrolling upwards our origin is already at the top of the head note (which is the intended location),
|
||||
// but when scrolling downwards our origin is at the _bottom_ of the tail note (where we need to be at the _top_ of the tail note)
|
||||
if (direction.Value == ScrollingDirection.Down)
|
||||
Y -= DrawableObject.Tail.DrawHeight;
|
||||
}
|
||||
head.Y = HitObjectContainer.PositionAtTime(HitObject.Head.StartTime, HitObject.StartTime);
|
||||
tail.Y = HitObjectContainer.PositionAtTime(HitObject.Tail.StartTime, HitObject.StartTime);
|
||||
Height = HitObjectContainer.LengthAtTime(HitObject.StartTime, HitObject.EndTime) + tail.DrawHeight;
|
||||
}
|
||||
|
||||
public override Quad SelectionQuad => ScreenSpaceDrawQuad;
|
||||
|
||||
public override Vector2 ScreenSpaceSelectionPoint => DrawableObject.Head.ScreenSpaceDrawQuad.Centre;
|
||||
public override Vector2 ScreenSpaceSelectionPoint => head.ScreenSpaceDrawQuad.Centre;
|
||||
}
|
||||
}
|
||||
|
@ -5,20 +5,23 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
{
|
||||
public abstract class ManiaSelectionBlueprint<T> : HitObjectSelectionBlueprint<T>
|
||||
where T : ManiaHitObject
|
||||
{
|
||||
public new DrawableManiaHitObject DrawableObject => (DrawableManiaHitObject)base.DrawableObject;
|
||||
[Resolved]
|
||||
private Playfield playfield { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private IScrollingInfo scrollingInfo { get; set; }
|
||||
|
||||
protected ScrollingHitObjectContainer HitObjectContainer => ((ManiaPlayfield)playfield).GetColumn(HitObject.Column).HitObjectContainer;
|
||||
|
||||
protected ManiaSelectionBlueprint(T hitObject)
|
||||
: base(hitObject)
|
||||
{
|
||||
@ -29,19 +32,13 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
{
|
||||
base.Update();
|
||||
|
||||
Position = Parent.ToLocalSpace(DrawableObject.ToScreenSpace(Vector2.Zero));
|
||||
}
|
||||
var anchor = scrollingInfo.Direction.Value == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
|
||||
Anchor = Origin = anchor;
|
||||
foreach (var child in InternalChildren)
|
||||
child.Anchor = child.Origin = anchor;
|
||||
|
||||
public override void Show()
|
||||
{
|
||||
DrawableObject.AlwaysAlive = true;
|
||||
base.Show();
|
||||
}
|
||||
|
||||
public override void Hide()
|
||||
{
|
||||
DrawableObject.AlwaysAlive = false;
|
||||
base.Hide();
|
||||
Position = Parent.ToLocalSpace(HitObjectContainer.ScreenSpacePositionAtTime(HitObject.StartTime)) - AnchorPosition;
|
||||
Width = HitObjectContainer.DrawWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,14 +14,5 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
{
|
||||
AddInternal(new EditNotePiece { RelativeSizeAxes = Axes.X });
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
// Todo: This shouldn't exist, mania should not reference the drawable hitobject directly.
|
||||
if (DrawableObject.IsLoaded)
|
||||
Size = DrawableObject.DrawSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -85,63 +85,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
AccentColour.UnbindFrom(ParentHitObject.AccentColour);
|
||||
}
|
||||
|
||||
private double computedLifetimeStart;
|
||||
|
||||
public override double LifetimeStart
|
||||
{
|
||||
get => base.LifetimeStart;
|
||||
set
|
||||
{
|
||||
computedLifetimeStart = value;
|
||||
|
||||
if (!AlwaysAlive)
|
||||
base.LifetimeStart = value;
|
||||
}
|
||||
}
|
||||
|
||||
private double computedLifetimeEnd;
|
||||
|
||||
public override double LifetimeEnd
|
||||
{
|
||||
get => base.LifetimeEnd;
|
||||
set
|
||||
{
|
||||
computedLifetimeEnd = value;
|
||||
|
||||
if (!AlwaysAlive)
|
||||
base.LifetimeEnd = value;
|
||||
}
|
||||
}
|
||||
|
||||
private bool alwaysAlive;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this <see cref="DrawableManiaHitObject"/> should always remain alive.
|
||||
/// </summary>
|
||||
internal bool AlwaysAlive
|
||||
{
|
||||
get => alwaysAlive;
|
||||
set
|
||||
{
|
||||
if (alwaysAlive == value)
|
||||
return;
|
||||
|
||||
alwaysAlive = value;
|
||||
|
||||
if (value)
|
||||
{
|
||||
// Set the base lifetimes directly, to avoid mangling the computed lifetimes
|
||||
base.LifetimeStart = double.MinValue;
|
||||
base.LifetimeEnd = double.MaxValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
LifetimeStart = computedLifetimeStart;
|
||||
LifetimeEnd = computedLifetimeEnd;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void OnDirectionChanged(ValueChangedEvent<ScrollingDirection> e)
|
||||
{
|
||||
Anchor = Origin = e.NewValue == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
|
||||
|
@ -0,0 +1,114 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Transforms;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Edit;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestSceneOsuEditorHitAnimations : TestSceneOsuEditor
|
||||
{
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; }
|
||||
|
||||
[Test]
|
||||
public void TestHitCircleAnimationDisable()
|
||||
{
|
||||
HitCircle hitCircle = null;
|
||||
DrawableHitCircle drawableHitCircle = null;
|
||||
|
||||
AddStep("retrieve first hit circle", () => hitCircle = getHitCircle(0));
|
||||
toggleAnimations(true);
|
||||
seekSmoothlyTo(() => hitCircle.StartTime + 10);
|
||||
|
||||
AddStep("retrieve drawable", () => drawableHitCircle = (DrawableHitCircle)getDrawableObjectFor(hitCircle));
|
||||
assertFutureTransforms(() => drawableHitCircle.CirclePiece, true);
|
||||
|
||||
AddStep("retrieve second hit circle", () => hitCircle = getHitCircle(1));
|
||||
toggleAnimations(false);
|
||||
seekSmoothlyTo(() => hitCircle.StartTime + 10);
|
||||
|
||||
AddStep("retrieve drawable", () => drawableHitCircle = (DrawableHitCircle)getDrawableObjectFor(hitCircle));
|
||||
assertFutureTransforms(() => drawableHitCircle.CirclePiece, false);
|
||||
AddAssert("hit circle has longer fade-out applied", () =>
|
||||
{
|
||||
var alphaTransform = drawableHitCircle.Transforms.Last(t => t.TargetMember == nameof(Alpha));
|
||||
return alphaTransform.EndTime - alphaTransform.StartTime == DrawableOsuEditorRuleset.EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION;
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliderAnimationDisable()
|
||||
{
|
||||
Slider slider = null;
|
||||
DrawableSlider drawableSlider = null;
|
||||
DrawableSliderRepeat sliderRepeat = null;
|
||||
|
||||
AddStep("retrieve first slider with repeats", () => slider = getSliderWithRepeats(0));
|
||||
toggleAnimations(true);
|
||||
seekSmoothlyTo(() => slider.StartTime + slider.SpanDuration + 10);
|
||||
|
||||
retrieveDrawables();
|
||||
assertFutureTransforms(() => sliderRepeat, true);
|
||||
|
||||
AddStep("retrieve second slider with repeats", () => slider = getSliderWithRepeats(1));
|
||||
toggleAnimations(false);
|
||||
seekSmoothlyTo(() => slider.StartTime + slider.SpanDuration + 10);
|
||||
|
||||
retrieveDrawables();
|
||||
assertFutureTransforms(() => sliderRepeat.Arrow, false);
|
||||
seekSmoothlyTo(() => slider.GetEndTime());
|
||||
AddAssert("slider has longer fade-out applied", () =>
|
||||
{
|
||||
var alphaTransform = drawableSlider.Transforms.Last(t => t.TargetMember == nameof(Alpha));
|
||||
return alphaTransform.EndTime - alphaTransform.StartTime == DrawableOsuEditorRuleset.EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION;
|
||||
});
|
||||
|
||||
void retrieveDrawables() =>
|
||||
AddStep("retrieve drawables", () =>
|
||||
{
|
||||
drawableSlider = (DrawableSlider)getDrawableObjectFor(slider);
|
||||
sliderRepeat = (DrawableSliderRepeat)getDrawableObjectFor(slider.NestedHitObjects.OfType<SliderRepeat>().First());
|
||||
});
|
||||
}
|
||||
|
||||
private HitCircle getHitCircle(int index)
|
||||
=> EditorBeatmap.HitObjects.OfType<HitCircle>().ElementAt(index);
|
||||
|
||||
private Slider getSliderWithRepeats(int index)
|
||||
=> EditorBeatmap.HitObjects.OfType<Slider>().Where(s => s.RepeatCount >= 1).ElementAt(index);
|
||||
|
||||
private DrawableHitObject getDrawableObjectFor(HitObject hitObject)
|
||||
=> this.ChildrenOfType<DrawableHitObject>().Single(ho => ho.HitObject == hitObject);
|
||||
|
||||
private IEnumerable<Transform> getTransformsRecursively(Drawable drawable)
|
||||
=> drawable.ChildrenOfType<Drawable>().SelectMany(d => d.Transforms);
|
||||
|
||||
private void toggleAnimations(bool enabled)
|
||||
=> AddStep($"toggle animations {(enabled ? "on" : "off")}", () => config.SetValue(OsuSetting.EditorHitAnimations, enabled));
|
||||
|
||||
private void seekSmoothlyTo(Func<double> targetTime)
|
||||
{
|
||||
AddStep("seek smoothly", () => EditorClock.SeekSmoothlyTo(targetTime.Invoke()));
|
||||
AddUntilStep("wait for seek", () => Precision.AlmostEquals(targetTime.Invoke(), EditorClock.CurrentTime));
|
||||
}
|
||||
|
||||
private void assertFutureTransforms(Func<Drawable> getDrawable, bool hasFutureTransforms)
|
||||
=> AddAssert($"object {(hasFutureTransforms ? "has" : "has no")} future transforms",
|
||||
() => getTransformsRecursively(getDrawable()).Any(t => t.EndTime >= EditorClock.CurrentTime) == hasFutureTransforms);
|
||||
}
|
||||
}
|
@ -21,20 +21,37 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
private int depthIndex;
|
||||
|
||||
[Test]
|
||||
public void TestVariousHitCircles()
|
||||
public void TestHits()
|
||||
{
|
||||
AddStep("Hit Big Single", () => SetContents(_ => testSingle(2, true)));
|
||||
AddStep("Hit Medium Single", () => SetContents(_ => testSingle(5, true)));
|
||||
AddStep("Hit Small Single", () => SetContents(_ => testSingle(7, true)));
|
||||
AddStep("Hit Big Stream", () => SetContents(_ => testStream(2, true)));
|
||||
AddStep("Hit Medium Stream", () => SetContents(_ => testStream(5, true)));
|
||||
AddStep("Hit Small Stream", () => SetContents(_ => testStream(7, true)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHittingEarly()
|
||||
{
|
||||
AddStep("Hit stream early", () => SetContents(_ => testStream(5, true, -150)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMisses()
|
||||
{
|
||||
AddStep("Miss Big Single", () => SetContents(_ => testSingle(2)));
|
||||
AddStep("Miss Medium Single", () => SetContents(_ => testSingle(5)));
|
||||
AddStep("Miss Small Single", () => SetContents(_ => testSingle(7)));
|
||||
AddStep("Hit Big Single", () => SetContents(_ => testSingle(2, true)));
|
||||
AddStep("Hit Medium Single", () => SetContents(_ => testSingle(5, true)));
|
||||
AddStep("Hit Small Single", () => SetContents(_ => testSingle(7, true)));
|
||||
AddStep("Miss Big Stream", () => SetContents(_ => testStream(2)));
|
||||
AddStep("Miss Medium Stream", () => SetContents(_ => testStream(5)));
|
||||
AddStep("Miss Small Stream", () => SetContents(_ => testStream(7)));
|
||||
AddStep("Hit Big Stream", () => SetContents(_ => testStream(2, true)));
|
||||
AddStep("Hit Medium Stream", () => SetContents(_ => testStream(5, true)));
|
||||
AddStep("Hit Small Stream", () => SetContents(_ => testStream(7, true)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHittingLate()
|
||||
{
|
||||
AddStep("Hit stream late", () => SetContents(_ => testStream(5, true, 150)));
|
||||
}
|
||||
|
||||
private Drawable testSingle(float circleSize, bool auto = false, double timeOffset = 0, Vector2? positionOffset = null)
|
||||
@ -46,7 +63,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
return playfield;
|
||||
}
|
||||
|
||||
private Drawable testStream(float circleSize, bool auto = false)
|
||||
private Drawable testStream(float circleSize, bool auto = false, double hitOffset = 0)
|
||||
{
|
||||
var playfield = new TestOsuPlayfield();
|
||||
|
||||
@ -54,14 +71,14 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
for (int i = 0; i <= 1000; i += 100)
|
||||
{
|
||||
playfield.Add(createSingle(circleSize, auto, i, pos));
|
||||
playfield.Add(createSingle(circleSize, auto, i, pos, hitOffset));
|
||||
pos.X += 50;
|
||||
}
|
||||
|
||||
return playfield;
|
||||
}
|
||||
|
||||
private TestDrawableHitCircle createSingle(float circleSize, bool auto, double timeOffset, Vector2? positionOffset)
|
||||
private TestDrawableHitCircle createSingle(float circleSize, bool auto, double timeOffset, Vector2? positionOffset, double hitOffset = 0)
|
||||
{
|
||||
positionOffset ??= Vector2.Zero;
|
||||
|
||||
@ -73,14 +90,14 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
circle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = circleSize });
|
||||
|
||||
var drawable = CreateDrawableHitCircle(circle, auto);
|
||||
var drawable = CreateDrawableHitCircle(circle, auto, hitOffset);
|
||||
|
||||
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObjects>())
|
||||
mod.ApplyToDrawableHitObjects(new[] { drawable });
|
||||
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObject>())
|
||||
mod.ApplyToDrawableHitObject(drawable);
|
||||
return drawable;
|
||||
}
|
||||
|
||||
protected virtual TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto) => new TestDrawableHitCircle(circle, auto)
|
||||
protected virtual TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto, double hitOffset = 0) => new TestDrawableHitCircle(circle, auto, hitOffset)
|
||||
{
|
||||
Depth = depthIndex++
|
||||
};
|
||||
@ -88,18 +105,20 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
protected class TestDrawableHitCircle : DrawableHitCircle
|
||||
{
|
||||
private readonly bool auto;
|
||||
private readonly double hitOffset;
|
||||
|
||||
public TestDrawableHitCircle(HitCircle h, bool auto)
|
||||
public TestDrawableHitCircle(HitCircle h, bool auto, double hitOffset)
|
||||
: base(h)
|
||||
{
|
||||
this.auto = auto;
|
||||
this.hitOffset = hitOffset;
|
||||
}
|
||||
|
||||
public void TriggerJudgement() => UpdateResult(true);
|
||||
public void TriggerJudgement() => Schedule(() => UpdateResult(true));
|
||||
|
||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||
{
|
||||
if (auto && !userTriggered && timeOffset > 0)
|
||||
if (auto && !userTriggered && timeOffset > hitOffset && CheckHittable?.Invoke(this, Time.Current) != false)
|
||||
{
|
||||
// force success
|
||||
ApplyResult(r => r.Type = HitResult.Great);
|
||||
|
@ -16,11 +16,11 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
Scheduler.AddDelayed(() => comboIndex.Value++, 250, true);
|
||||
}
|
||||
|
||||
protected override TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto)
|
||||
protected override TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto, double hitOffset = 0)
|
||||
{
|
||||
circle.ComboIndexBindable.BindTo(comboIndex);
|
||||
circle.IndexInCurrentComboBindable.BindTo(comboIndex);
|
||||
return base.CreateDrawableHitCircle(circle, auto);
|
||||
return base.CreateDrawableHitCircle(circle, auto, hitOffset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,9 +26,9 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
return base.CreateBeatmapForSkinProvider();
|
||||
}
|
||||
|
||||
protected override TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto)
|
||||
protected override TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto, double hitOffset = 0)
|
||||
{
|
||||
var drawableHitObject = base.CreateDrawableHitCircle(circle, auto);
|
||||
var drawableHitObject = base.CreateDrawableHitCircle(circle, auto, hitOffset);
|
||||
|
||||
Debug.Assert(drawableHitObject.HitObject.HitWindows != null);
|
||||
|
||||
|
@ -335,8 +335,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
var drawable = CreateDrawableSlider(slider);
|
||||
|
||||
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObjects>())
|
||||
mod.ApplyToDrawableHitObjects(new[] { drawable });
|
||||
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObject>())
|
||||
mod.ApplyToDrawableHitObject(drawable);
|
||||
|
||||
drawable.OnNewResult += onNewResult;
|
||||
|
||||
|
@ -85,8 +85,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
Scale = new Vector2(0.75f)
|
||||
};
|
||||
|
||||
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObjects>())
|
||||
mod.ApplyToDrawableHitObjects(new[] { drawableSpinner });
|
||||
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObject>())
|
||||
mod.ApplyToDrawableHitObject(drawableSpinner);
|
||||
|
||||
return drawableSpinner;
|
||||
}
|
||||
|
@ -20,6 +20,12 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
public class DrawableOsuEditorRuleset : DrawableOsuRuleset
|
||||
{
|
||||
/// <summary>
|
||||
/// Hit objects are intentionally made to fade out at a constant slower rate than in gameplay.
|
||||
/// This allows a mapper to gain better historical context and use recent hitobjects as reference / snap points.
|
||||
/// </summary>
|
||||
public const double EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION = 700;
|
||||
|
||||
public DrawableOsuEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods)
|
||||
: base(ruleset, beatmap, mods)
|
||||
{
|
||||
@ -46,12 +52,6 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
d.ApplyCustomUpdateState += updateState;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hit objects are intentionally made to fade out at a constant slower rate than in gameplay.
|
||||
/// This allows a mapper to gain better historical context and use recent hitobjects as reference / snap points.
|
||||
/// </summary>
|
||||
private const double editor_hit_object_fade_out_extension = 700;
|
||||
|
||||
private void updateState(DrawableHitObject hitObject, ArmedState state)
|
||||
{
|
||||
if (state == ArmedState.Idle || hitAnimations.Value)
|
||||
@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
if (hitObject is DrawableHitCircle circle)
|
||||
{
|
||||
circle.ApproachCircle
|
||||
.FadeOutFromOne(editor_hit_object_fade_out_extension * 4)
|
||||
.FadeOutFromOne(EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION * 4)
|
||||
.Expire();
|
||||
|
||||
circle.ApproachCircle.ScaleTo(1.1f, 300, Easing.OutQuint);
|
||||
@ -69,14 +69,20 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
if (hitObject is IHasMainCirclePiece mainPieceContainer)
|
||||
{
|
||||
// clear any explode animation logic.
|
||||
mainPieceContainer.CirclePiece.ApplyTransformsAt(hitObject.HitStateUpdateTime, true);
|
||||
mainPieceContainer.CirclePiece.ClearTransformsAfter(hitObject.HitStateUpdateTime, true);
|
||||
// this is scheduled after children to ensure that the clear happens after invocations of ApplyCustomUpdateState on the circle piece's nested skinnables.
|
||||
ScheduleAfterChildren(() =>
|
||||
{
|
||||
if (hitObject.HitObject == null) return;
|
||||
|
||||
mainPieceContainer.CirclePiece.ApplyTransformsAt(hitObject.StateUpdateTime, true);
|
||||
mainPieceContainer.CirclePiece.ClearTransformsAfter(hitObject.StateUpdateTime, true);
|
||||
});
|
||||
}
|
||||
|
||||
if (hitObject is DrawableSliderRepeat repeat)
|
||||
{
|
||||
repeat.Arrow.ApplyTransformsAt(hitObject.HitStateUpdateTime, true);
|
||||
repeat.Arrow.ClearTransformsAfter(hitObject.HitStateUpdateTime, true);
|
||||
repeat.Arrow.ApplyTransformsAt(hitObject.StateUpdateTime, true);
|
||||
repeat.Arrow.ClearTransformsAfter(hitObject.StateUpdateTime, true);
|
||||
}
|
||||
|
||||
// adjust the visuals of top-level object types to make them stay on screen for longer than usual.
|
||||
@ -93,7 +99,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
hitObject.RemoveTransform(existing);
|
||||
|
||||
using (hitObject.BeginAbsoluteSequence(hitObject.HitStateUpdateTime))
|
||||
hitObject.FadeOut(editor_hit_object_fade_out_extension).Expire();
|
||||
hitObject.FadeOut(EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION).Expire();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
12
osu.Game.Rulesets.Osu/Mods/IMutateApproachCircles.cs
Normal file
12
osu.Game.Rulesets.Osu/Mods/IMutateApproachCircles.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.
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
/// <summary>
|
||||
/// Any mod which affects the animation or visibility of approach circles. Should be used for incompatibility purposes.
|
||||
/// </summary>
|
||||
public interface IMutateApproachCircles
|
||||
{
|
||||
}
|
||||
}
|
100
osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs
Normal file
100
osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.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 System;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModApproachDifferent : Mod, IApplicableToDrawableHitObject, IMutateApproachCircles
|
||||
{
|
||||
public override string Name => "Approach Different";
|
||||
public override string Acronym => "AD";
|
||||
public override string Description => "Never trust the approach circles...";
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override IconUsage? Icon { get; } = FontAwesome.Regular.Circle;
|
||||
|
||||
public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) };
|
||||
|
||||
[SettingSource("Initial size", "Change the initial size of the approach circle, relative to hit circles.", 0)]
|
||||
public BindableFloat Scale { get; } = new BindableFloat(4)
|
||||
{
|
||||
Precision = 0.1f,
|
||||
MinValue = 2,
|
||||
MaxValue = 10,
|
||||
};
|
||||
|
||||
[SettingSource("Style", "Change the animation style of the approach circles.", 1)]
|
||||
public Bindable<AnimationStyle> Style { get; } = new Bindable<AnimationStyle>();
|
||||
|
||||
public void ApplyToDrawableHitObject(DrawableHitObject drawable)
|
||||
{
|
||||
drawable.ApplyCustomUpdateState += (drawableObject, state) =>
|
||||
{
|
||||
if (!(drawableObject is DrawableHitCircle drawableHitCircle)) return;
|
||||
|
||||
var hitCircle = drawableHitCircle.HitObject;
|
||||
|
||||
drawableHitCircle.ApproachCircle.ClearTransforms(targetMember: nameof(Scale));
|
||||
|
||||
using (drawableHitCircle.BeginAbsoluteSequence(hitCircle.StartTime - hitCircle.TimePreempt))
|
||||
drawableHitCircle.ApproachCircle.ScaleTo(Scale.Value).ScaleTo(1f, hitCircle.TimePreempt, getEasing(Style.Value));
|
||||
};
|
||||
}
|
||||
|
||||
private Easing getEasing(AnimationStyle style)
|
||||
{
|
||||
switch (style)
|
||||
{
|
||||
default:
|
||||
return Easing.None;
|
||||
|
||||
case AnimationStyle.Accelerate1:
|
||||
return Easing.In;
|
||||
|
||||
case AnimationStyle.Accelerate2:
|
||||
return Easing.InCubic;
|
||||
|
||||
case AnimationStyle.Accelerate3:
|
||||
return Easing.InQuint;
|
||||
|
||||
case AnimationStyle.Gravity:
|
||||
return Easing.InBack;
|
||||
|
||||
case AnimationStyle.Decelerate1:
|
||||
return Easing.Out;
|
||||
|
||||
case AnimationStyle.Decelerate2:
|
||||
return Easing.OutCubic;
|
||||
|
||||
case AnimationStyle.Decelerate3:
|
||||
return Easing.OutQuint;
|
||||
|
||||
case AnimationStyle.InOut1:
|
||||
return Easing.InOutCubic;
|
||||
|
||||
case AnimationStyle.InOut2:
|
||||
return Easing.InOutQuint;
|
||||
}
|
||||
}
|
||||
|
||||
public enum AnimationStyle
|
||||
{
|
||||
Gravity,
|
||||
InOut1,
|
||||
InOut2,
|
||||
Accelerate1,
|
||||
Accelerate2,
|
||||
Accelerate3,
|
||||
Decelerate1,
|
||||
Decelerate2,
|
||||
Decelerate3,
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
@ -9,22 +8,19 @@ using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModBarrelRoll : ModBarrelRoll<OsuHitObject>, IApplicableToDrawableHitObjects
|
||||
public class OsuModBarrelRoll : ModBarrelRoll<OsuHitObject>, IApplicableToDrawableHitObject
|
||||
{
|
||||
public void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables)
|
||||
public void ApplyToDrawableHitObject(DrawableHitObject d)
|
||||
{
|
||||
foreach (var d in drawables)
|
||||
d.OnUpdate += _ =>
|
||||
{
|
||||
d.OnUpdate += _ =>
|
||||
switch (d)
|
||||
{
|
||||
switch (d)
|
||||
{
|
||||
case DrawableHitCircle circle:
|
||||
circle.CirclePiece.Rotation = -CurrentRotation;
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
case DrawableHitCircle circle:
|
||||
circle.CirclePiece.Rotation = -CurrentRotation;
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Configuration;
|
||||
@ -15,7 +14,7 @@ using osu.Game.Rulesets.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModClassic : ModClassic, IApplicableToHitObject, IApplicableToDrawableHitObjects, IApplicableToDrawableRuleset<OsuHitObject>
|
||||
public class OsuModClassic : ModClassic, IApplicableToHitObject, IApplicableToDrawableHitObject, IApplicableToDrawableRuleset<OsuHitObject>
|
||||
{
|
||||
[SettingSource("No slider head accuracy requirement", "Scores sliders proportionally to the number of ticks hit.")]
|
||||
public Bindable<bool> NoSliderHeadAccuracy { get; } = new BindableBool(true);
|
||||
@ -54,24 +53,21 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
osuRuleset.Playfield.HitPolicy = new ObjectOrderedHitPolicy();
|
||||
}
|
||||
|
||||
public void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables)
|
||||
public void ApplyToDrawableHitObject(DrawableHitObject obj)
|
||||
{
|
||||
foreach (var obj in drawables)
|
||||
switch (obj)
|
||||
{
|
||||
switch (obj)
|
||||
{
|
||||
case DrawableSlider slider:
|
||||
slider.Ball.InputTracksVisualSize = !FixedFollowCircleHitArea.Value;
|
||||
break;
|
||||
case DrawableSlider slider:
|
||||
slider.Ball.InputTracksVisualSize = !FixedFollowCircleHitArea.Value;
|
||||
break;
|
||||
|
||||
case DrawableSliderHead head:
|
||||
head.TrackFollowCircle = !NoSliderHeadMovement.Value;
|
||||
break;
|
||||
case DrawableSliderHead head:
|
||||
head.TrackFollowCircle = !NoSliderHeadMovement.Value;
|
||||
break;
|
||||
|
||||
case DrawableSliderTail tail:
|
||||
tail.SamplePlaysOnlyOnHit = !AlwaysPlayTailSample.Value;
|
||||
break;
|
||||
}
|
||||
case DrawableSliderTail tail:
|
||||
tail.SamplePlaysOnlyOnHit = !AlwaysPlayTailSample.Value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,8 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input;
|
||||
@ -19,7 +17,7 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModFlashlight : ModFlashlight<OsuHitObject>, IApplicableToDrawableHitObjects
|
||||
public class OsuModFlashlight : ModFlashlight<OsuHitObject>, IApplicableToDrawableHitObject
|
||||
{
|
||||
public override double ScoreMultiplier => 1.12;
|
||||
|
||||
@ -31,12 +29,10 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
public override Flashlight CreateFlashlight() => flashlight = new OsuFlashlight();
|
||||
|
||||
public void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables)
|
||||
public void ApplyToDrawableHitObject(DrawableHitObject drawable)
|
||||
{
|
||||
foreach (var s in drawables.OfType<DrawableSlider>())
|
||||
{
|
||||
if (drawable is DrawableSlider s)
|
||||
s.Tracking.ValueChanged += flashlight.OnSliderTrackingChange;
|
||||
}
|
||||
}
|
||||
|
||||
public override void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
|
||||
|
@ -11,15 +11,16 @@ using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModHidden : ModHidden
|
||||
public class OsuModHidden : ModHidden, IMutateApproachCircles
|
||||
{
|
||||
public override string Description => @"Play with no approach circles and fading circles/sliders.";
|
||||
public override double ScoreMultiplier => 1.06;
|
||||
|
||||
public override Type[] IncompatibleMods => new[] { typeof(OsuModTraceable), typeof(OsuModSpinIn) };
|
||||
public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) };
|
||||
|
||||
private const double fade_in_duration_multiplier = 0.4;
|
||||
private const double fade_out_duration_multiplier = 0.3;
|
||||
@ -110,6 +111,9 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
// hide elements we don't care about.
|
||||
// todo: hide background
|
||||
|
||||
spinner.Body.OnSkinChanged += () => hideSpinnerApproachCircle(spinner);
|
||||
hideSpinnerApproachCircle(spinner);
|
||||
|
||||
using (spinner.BeginAbsoluteSequence(fadeStartTime))
|
||||
spinner.FadeOut(fadeDuration);
|
||||
|
||||
@ -160,5 +164,15 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void hideSpinnerApproachCircle(DrawableSpinner spinner)
|
||||
{
|
||||
var approachCircle = (spinner.Body.Drawable as IHasApproachCircle)?.ApproachCircle;
|
||||
if (approachCircle == null)
|
||||
return;
|
||||
|
||||
using (spinner.BeginAbsoluteSequence(spinner.HitObject.StartTime - spinner.HitObject.TimePreempt))
|
||||
approachCircle.Hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
/// <summary>
|
||||
/// Adjusts the size of hit objects during their fade in animation.
|
||||
/// </summary>
|
||||
public abstract class OsuModObjectScaleTween : ModWithVisibilityAdjustment
|
||||
public abstract class OsuModObjectScaleTween : ModWithVisibilityAdjustment, IMutateApproachCircles
|
||||
{
|
||||
public override ModType Type => ModType.Fun;
|
||||
|
||||
@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
protected virtual float EndScale => 1;
|
||||
|
||||
public override Type[] IncompatibleMods => new[] { typeof(OsuModSpinIn), typeof(OsuModTraceable) };
|
||||
public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) };
|
||||
|
||||
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
|
||||
{
|
||||
|
@ -12,7 +12,7 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModSpinIn : ModWithVisibilityAdjustment
|
||||
public class OsuModSpinIn : ModWithVisibilityAdjustment, IMutateApproachCircles
|
||||
{
|
||||
public override string Name => "Spin In";
|
||||
public override string Acronym => "SI";
|
||||
@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
// todo: this mod should be able to be compatible with hidden with a bit of further implementation.
|
||||
public override Type[] IncompatibleMods => new[] { typeof(OsuModObjectScaleTween), typeof(OsuModHidden), typeof(OsuModTraceable) };
|
||||
public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) };
|
||||
|
||||
private const int rotate_offset = 360;
|
||||
private const float rotate_starting_width = 2;
|
||||
|
@ -2,7 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Utils;
|
||||
@ -13,7 +12,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModSpunOut : Mod, IApplicableToDrawableHitObjects
|
||||
public class OsuModSpunOut : Mod, IApplicableToDrawableHitObject
|
||||
{
|
||||
public override string Name => "Spun Out";
|
||||
public override string Acronym => "SO";
|
||||
@ -23,15 +22,12 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public override double ScoreMultiplier => 0.9;
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(OsuModAutopilot) };
|
||||
|
||||
public void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables)
|
||||
public void ApplyToDrawableHitObject(DrawableHitObject hitObject)
|
||||
{
|
||||
foreach (var hitObject in drawables)
|
||||
if (hitObject is DrawableSpinner spinner)
|
||||
{
|
||||
if (hitObject is DrawableSpinner spinner)
|
||||
{
|
||||
spinner.HandleUserInput = false;
|
||||
spinner.OnUpdate += onSpinnerUpdate;
|
||||
}
|
||||
spinner.HandleUserInput = false;
|
||||
spinner.OnUpdate += onSpinnerUpdate;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -11,7 +11,7 @@ using osu.Game.Rulesets.Osu.Skinning.Default;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModTraceable : ModWithVisibilityAdjustment
|
||||
public class OsuModTraceable : ModWithVisibilityAdjustment, IMutateApproachCircles
|
||||
{
|
||||
public override string Name => "Traceable";
|
||||
public override string Acronym => "TC";
|
||||
@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public override string Description => "Put your faith in the approach circles...";
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
public override Type[] IncompatibleMods => new[] { typeof(OsuModHidden), typeof(OsuModSpinIn), typeof(OsuModObjectScaleTween) };
|
||||
public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) };
|
||||
|
||||
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
|
||||
{
|
||||
|
@ -12,6 +12,7 @@ using osu.Framework.Input.Bindings;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Rulesets.Osu.Skinning;
|
||||
using osu.Game.Rulesets.Osu.Skinning.Default;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Skinning;
|
||||
@ -19,7 +20,7 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
public class DrawableHitCircle : DrawableOsuHitObject, IHasMainCirclePiece
|
||||
public class DrawableHitCircle : DrawableOsuHitObject, IHasMainCirclePiece, IHasApproachCircle
|
||||
{
|
||||
public OsuAction? HitAction => HitArea.HitAction;
|
||||
protected virtual OsuSkinComponents CirclePieceComponent => OsuSkinComponents.HitCircle;
|
||||
@ -28,6 +29,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
public HitReceptor HitArea { get; private set; }
|
||||
public SkinnableDrawable CirclePiece { get; private set; }
|
||||
|
||||
Drawable IHasApproachCircle.ApproachCircle => ApproachCircle;
|
||||
|
||||
private Container scaleContainer;
|
||||
private InputManager inputManager;
|
||||
|
||||
@ -172,6 +175,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
base.UpdateStartTimeStateTransforms();
|
||||
|
||||
// always fade out at the circle's start time (to match user expectations).
|
||||
ApproachCircle.FadeOut(50);
|
||||
}
|
||||
|
||||
@ -182,6 +186,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
// todo: temporary / arbitrary, used for lifetime optimisation.
|
||||
this.Delay(800).FadeOut();
|
||||
|
||||
// in the case of an early state change, the fade should be expedited to the current point in time.
|
||||
if (HitStateUpdateTime < HitObject.StartTime)
|
||||
ApproachCircle.FadeOut(50);
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case ArmedState.Idle:
|
||||
|
@ -29,6 +29,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
public new OsuSpinnerJudgementResult Result => (OsuSpinnerJudgementResult)base.Result;
|
||||
|
||||
public SkinnableDrawable Body { get; private set; }
|
||||
|
||||
public SpinnerRotationTracker RotationTracker { get; private set; }
|
||||
|
||||
private SpinnerSpmCalculator spmCalculator;
|
||||
@ -86,7 +88,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SpinnerBody), _ => new DefaultSpinner()),
|
||||
Body = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SpinnerBody), _ => new DefaultSpinner()),
|
||||
RotationTracker = new SpinnerRotationTracker(this)
|
||||
}
|
||||
},
|
||||
|
@ -187,6 +187,7 @@ namespace osu.Game.Rulesets.Osu
|
||||
new MultiMod(new ModWindUp(), new ModWindDown()),
|
||||
new OsuModTraceable(),
|
||||
new OsuModBarrelRoll(),
|
||||
new OsuModApproachDifferent(),
|
||||
};
|
||||
|
||||
case ModType.System:
|
||||
|
@ -10,7 +10,6 @@ namespace osu.Game.Rulesets.Osu
|
||||
Cursor,
|
||||
CursorTrail,
|
||||
SliderScorePoint,
|
||||
ApproachCircle,
|
||||
ReverseArrow,
|
||||
HitCircleText,
|
||||
SliderHeadHitCircle,
|
||||
|
18
osu.Game.Rulesets.Osu/Skinning/IHasApproachCircle.cs
Normal file
18
osu.Game.Rulesets.Osu/Skinning/IHasApproachCircle.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.Framework.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Skinning
|
||||
{
|
||||
/// <summary>
|
||||
/// A common interface between implementations which provide an approach circle.
|
||||
/// </summary>
|
||||
public interface IHasApproachCircle
|
||||
{
|
||||
/// <summary>
|
||||
/// The approach circle drawable.
|
||||
/// </summary>
|
||||
Drawable ApproachCircle { get; }
|
||||
}
|
||||
}
|
@ -55,28 +55,40 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Texture = source.GetTexture("spinner-bottom")
|
||||
Texture = source.GetTexture("spinner-bottom"),
|
||||
},
|
||||
discTop = new Sprite
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Texture = source.GetTexture("spinner-top")
|
||||
Texture = source.GetTexture("spinner-top"),
|
||||
},
|
||||
fixedMiddle = new Sprite
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Texture = source.GetTexture("spinner-middle")
|
||||
Texture = source.GetTexture("spinner-middle"),
|
||||
},
|
||||
spinningMiddle = new Sprite
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Texture = source.GetTexture("spinner-middle2")
|
||||
}
|
||||
Texture = source.GetTexture("spinner-middle2"),
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
if (!(source.FindProvider(s => s.GetTexture("spinner-top") != null) is DefaultLegacySkin))
|
||||
{
|
||||
AddInternal(ApproachCircle = new Sprite
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.Centre,
|
||||
Texture = source.GetTexture("spinner-approachcircle"),
|
||||
Scale = new Vector2(SPRITE_SCALE * 1.86f),
|
||||
Y = SPINNER_Y_CENTRE,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected override void UpdateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
|
||||
|
@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
{
|
||||
spinnerBlink = source.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.SpinnerNoBlink)?.Value != true;
|
||||
|
||||
AddRangeInternal(new Drawable[]
|
||||
AddRangeInternal(new[]
|
||||
{
|
||||
new Sprite
|
||||
{
|
||||
@ -68,6 +68,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
Origin = Anchor.TopLeft,
|
||||
Scale = new Vector2(SPRITE_SCALE)
|
||||
}
|
||||
},
|
||||
ApproachCircle = new Sprite
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.Centre,
|
||||
Texture = source.GetTexture("spinner-approachcircle"),
|
||||
Scale = new Vector2(SPRITE_SCALE * 1.86f),
|
||||
Y = SPINNER_Y_CENTRE,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -15,8 +15,10 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
{
|
||||
public abstract class LegacySpinner : CompositeDrawable
|
||||
public abstract class LegacySpinner : CompositeDrawable, IHasApproachCircle
|
||||
{
|
||||
public const float SPRITE_SCALE = 0.625f;
|
||||
|
||||
/// <remarks>
|
||||
/// All constants are in osu!stable's gamefield space, which is shifted 16px downwards.
|
||||
/// This offset is negated in both osu!stable and osu!lazer to bring all constants into window-space.
|
||||
@ -26,12 +28,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
|
||||
protected const float SPINNER_Y_CENTRE = SPINNER_TOP_OFFSET + 219f;
|
||||
|
||||
protected const float SPRITE_SCALE = 0.625f;
|
||||
|
||||
private const float spm_hide_offset = 50f;
|
||||
|
||||
protected DrawableSpinner DrawableSpinner { get; private set; }
|
||||
|
||||
public Drawable ApproachCircle { get; protected set; }
|
||||
|
||||
private Sprite spin;
|
||||
private Sprite clear;
|
||||
|
||||
@ -175,6 +177,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
spmCounter.MoveToOffset(new Vector2(0, -spm_hide_offset), d.HitObject.TimeFadeIn, Easing.Out);
|
||||
}
|
||||
|
||||
using (BeginAbsoluteSequence(d.HitObject.StartTime))
|
||||
ApproachCircle?.ScaleTo(SPRITE_SCALE * 0.1f, d.HitObject.Duration);
|
||||
|
||||
double spinFadeOutLength = Math.Min(400, d.HitObject.Duration);
|
||||
|
||||
using (BeginAbsoluteSequence(drawableHitObject.HitStateUpdateTime - spinFadeOutLength, true))
|
||||
|
12
osu.Game.Rulesets.Taiko.Tests/Mods/TaikoModTestScene.cs
Normal file
12
osu.Game.Rulesets.Taiko.Tests/Mods/TaikoModTestScene.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.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Tests.Mods
|
||||
{
|
||||
public abstract class TaikoModTestScene : ModTestScene
|
||||
{
|
||||
protected sealed override Ruleset CreatePlayerRuleset() => new TaikoRuleset();
|
||||
}
|
||||
}
|
@ -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 System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Rulesets.Taiko.Mods;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Tests.Mods
|
||||
{
|
||||
public class TestSceneTaikoModHidden : TaikoModTestScene
|
||||
{
|
||||
[Test]
|
||||
public void TestDefaultBeatmapTest() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new TaikoModHidden(),
|
||||
Autoplay = true,
|
||||
PassCondition = checkSomeAutoplayHits
|
||||
});
|
||||
|
||||
private bool checkSomeAutoplayHits()
|
||||
=> Player.ScoreProcessor.JudgedHits >= 4
|
||||
&& Player.Results.All(result => result.Type == result.Judgement.MaxResult);
|
||||
}
|
||||
}
|
@ -1,23 +1,93 @@
|
||||
// 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.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Taiko.Objects.Drawables;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Mods
|
||||
{
|
||||
public class TaikoModHidden : ModHidden
|
||||
public class TaikoModHidden : ModHidden, IApplicableToDifficulty
|
||||
{
|
||||
public override string Description => @"Beats fade out before you hit them!";
|
||||
public override double ScoreMultiplier => 1.06;
|
||||
public override bool HasImplementation => false;
|
||||
|
||||
/// <summary>
|
||||
/// In osu-stable, the hit position is 160, so the active playfield is essentially 160 pixels shorter
|
||||
/// than the actual screen width. The normalized playfield height is 480, so on a 4:3 screen the
|
||||
/// playfield ratio of the active area up to the hit position will actually be (640 - 160) / 480 = 1.
|
||||
/// For custom resolutions/aspect ratios (x:y), the screen width given the normalized height becomes 480 * x / y instead,
|
||||
/// and the playfield ratio becomes (480 * x / y - 160) / 480 = x / y - 1/3.
|
||||
/// This constant is equal to the playfield ratio on 4:3 screens divided by the playfield ratio on 16:9 screens.
|
||||
/// </summary>
|
||||
private const double hd_sv_scale = (4.0 / 3.0 - 1.0 / 3.0) / (16.0 / 9.0 - 1.0 / 3.0);
|
||||
|
||||
private double originalSliderMultiplier;
|
||||
|
||||
private ControlPointInfo controlPointInfo;
|
||||
|
||||
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
|
||||
{
|
||||
ApplyNormalVisibilityState(hitObject, state);
|
||||
}
|
||||
|
||||
protected double MultiplierAt(double position)
|
||||
{
|
||||
double beatLength = controlPointInfo.TimingPointAt(position).BeatLength;
|
||||
double speedMultiplier = controlPointInfo.DifficultyPointAt(position).SpeedMultiplier;
|
||||
|
||||
return originalSliderMultiplier * speedMultiplier * TimingControlPoint.DEFAULT_BEAT_LENGTH / beatLength;
|
||||
}
|
||||
|
||||
protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state)
|
||||
{
|
||||
switch (hitObject)
|
||||
{
|
||||
case DrawableDrumRollTick _:
|
||||
case DrawableHit _:
|
||||
double preempt = 10000 / MultiplierAt(hitObject.HitObject.StartTime);
|
||||
double start = hitObject.HitObject.StartTime - preempt * 0.6;
|
||||
double duration = preempt * 0.3;
|
||||
|
||||
using (hitObject.BeginAbsoluteSequence(start))
|
||||
{
|
||||
hitObject.FadeOut(duration);
|
||||
|
||||
// DrawableHitObject sets LifetimeEnd to LatestTransformEndTime if it isn't manually changed.
|
||||
// in order for the object to not be killed before its actual end time (as the latest transform ends earlier), set lifetime end explicitly.
|
||||
hitObject.LifetimeEnd = state == ArmedState.Idle || !hitObject.AllJudged
|
||||
? hitObject.HitObject.GetEndTime() + hitObject.HitObject.HitWindows.WindowFor(HitResult.Miss)
|
||||
: hitObject.HitStateUpdateTime;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void ReadFromDifficulty(BeatmapDifficulty difficulty)
|
||||
{
|
||||
}
|
||||
|
||||
public void ApplyToDifficulty(BeatmapDifficulty difficulty)
|
||||
{
|
||||
// needs to be read after all processing has been run (TaikoBeatmapConverter applies an adjustment which would otherwise be omitted).
|
||||
originalSliderMultiplier = difficulty.SliderMultiplier;
|
||||
|
||||
// osu-stable has an added playfield cover that essentially forces a 4:3 playfield ratio, by cutting off all objects past that size.
|
||||
// This is not yet implemented; instead a playfield adjustment container is present which maintains a 16:9 ratio.
|
||||
// For now, increase the slider multiplier proportionally so that the notes stay on the screen for the same amount of time as on stable.
|
||||
// Note that this means that the notes will scroll faster as they have a longer distance to travel on the screen in that same amount of time.
|
||||
difficulty.SliderMultiplier /= hd_sv_scale;
|
||||
}
|
||||
|
||||
public override void ApplyToBeatmap(IBeatmap beatmap)
|
||||
{
|
||||
controlPointInfo = beatmap.ControlPointInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -28,6 +28,8 @@ namespace osu.Game.Tests.Chat
|
||||
[TestCase(LinkAction.OpenBeatmapSet, "123", "https://dev.ppy.sh/beatmapsets/123")]
|
||||
[TestCase(LinkAction.OpenBeatmapSet, "123", "https://dev.ppy.sh/beatmapsets/123/whatever")]
|
||||
[TestCase(LinkAction.External, "https://dev.ppy.sh/beatmapsets/abc", "https://dev.ppy.sh/beatmapsets/abc")]
|
||||
[TestCase(LinkAction.External, "https://dev.ppy.sh/beatmapsets/discussions", "https://dev.ppy.sh/beatmapsets/discussions")]
|
||||
[TestCase(LinkAction.External, "https://dev.ppy.sh/beatmapsets/discussions/123", "https://dev.ppy.sh/beatmapsets/discussions/123")]
|
||||
public void TestBeatmapLinks(LinkAction expectedAction, string expectedArg, string link)
|
||||
{
|
||||
MessageFormatter.WebsiteRootUrl = "dev.ppy.sh";
|
||||
|
101
osu.Game.Tests/Database/TestRealmKeyBindingStore.cs
Normal file
101
osu.Game.Tests/Database/TestRealmKeyBindingStore.cs
Normal file
@ -0,0 +1,101 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Input;
|
||||
using osu.Game.Input.Bindings;
|
||||
using Realms;
|
||||
|
||||
namespace osu.Game.Tests.Database
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestRealmKeyBindingStore
|
||||
{
|
||||
private NativeStorage storage;
|
||||
|
||||
private RealmKeyBindingStore keyBindingStore;
|
||||
|
||||
private RealmContextFactory realmContextFactory;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
var directory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()));
|
||||
|
||||
storage = new NativeStorage(directory.FullName);
|
||||
|
||||
realmContextFactory = new RealmContextFactory(storage);
|
||||
keyBindingStore = new RealmKeyBindingStore(realmContextFactory);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDefaultsPopulationAndQuery()
|
||||
{
|
||||
Assert.That(query().Count, Is.EqualTo(0));
|
||||
|
||||
KeyBindingContainer testContainer = new TestKeyBindingContainer();
|
||||
|
||||
keyBindingStore.Register(testContainer);
|
||||
|
||||
Assert.That(query().Count, Is.EqualTo(3));
|
||||
|
||||
Assert.That(query().Where(k => k.ActionInt == (int)GlobalAction.Back).Count, Is.EqualTo(1));
|
||||
Assert.That(query().Where(k => k.ActionInt == (int)GlobalAction.Select).Count, Is.EqualTo(2));
|
||||
}
|
||||
|
||||
private IQueryable<RealmKeyBinding> query() => realmContextFactory.Context.All<RealmKeyBinding>();
|
||||
|
||||
[Test]
|
||||
public void TestUpdateViaQueriedReference()
|
||||
{
|
||||
KeyBindingContainer testContainer = new TestKeyBindingContainer();
|
||||
|
||||
keyBindingStore.Register(testContainer);
|
||||
|
||||
var backBinding = query().Single(k => k.ActionInt == (int)GlobalAction.Back);
|
||||
|
||||
Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.Escape }));
|
||||
|
||||
var tsr = ThreadSafeReference.Create(backBinding);
|
||||
|
||||
using (var usage = realmContextFactory.GetForWrite())
|
||||
{
|
||||
var binding = usage.Realm.ResolveReference(tsr);
|
||||
binding.KeyCombination = new KeyCombination(InputKey.BackSpace);
|
||||
|
||||
usage.Commit();
|
||||
}
|
||||
|
||||
Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace }));
|
||||
|
||||
// check still correct after re-query.
|
||||
backBinding = query().Single(k => k.ActionInt == (int)GlobalAction.Back);
|
||||
Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace }));
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
realmContextFactory.Dispose();
|
||||
storage.DeleteDirectory(string.Empty);
|
||||
}
|
||||
|
||||
public class TestKeyBindingContainer : KeyBindingContainer
|
||||
{
|
||||
public override IEnumerable<IKeyBinding> DefaultKeyBindings =>
|
||||
new[]
|
||||
{
|
||||
new KeyBinding(InputKey.Escape, GlobalAction.Back),
|
||||
new KeyBinding(InputKey.Enter, GlobalAction.Select),
|
||||
new KeyBinding(InputKey.Space, GlobalAction.Select),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -44,11 +44,9 @@ namespace osu.Game.Tests.Gameplay
|
||||
{
|
||||
TestDrawableHitObject dho = null;
|
||||
TestLifetimeEntry entry = null;
|
||||
AddStep("Create DHO", () =>
|
||||
AddStep("Create DHO", () => Child = dho = new TestDrawableHitObject
|
||||
{
|
||||
dho = new TestDrawableHitObject(null);
|
||||
dho.Apply(entry = new TestLifetimeEntry(new HitObject()));
|
||||
Child = dho;
|
||||
Entry = entry = new TestLifetimeEntry(new HitObject())
|
||||
});
|
||||
|
||||
AddStep("KeepAlive = true", () =>
|
||||
@ -81,12 +79,10 @@ namespace osu.Game.Tests.Gameplay
|
||||
AddAssert("Lifetime is updated", () => entry.LifetimeStart == -TestLifetimeEntry.INITIAL_LIFETIME_OFFSET);
|
||||
|
||||
TestDrawableHitObject dho = null;
|
||||
AddStep("Create DHO", () =>
|
||||
AddStep("Create DHO", () => Child = dho = new TestDrawableHitObject
|
||||
{
|
||||
dho = new TestDrawableHitObject(null);
|
||||
dho.Apply(entry);
|
||||
Child = dho;
|
||||
dho.SetLifetimeStartOnApply = true;
|
||||
Entry = entry,
|
||||
SetLifetimeStartOnApply = true
|
||||
});
|
||||
AddStep("ApplyDefaults", () => entry.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()));
|
||||
AddAssert("Lifetime is correct", () => dho.LifetimeStart == TestDrawableHitObject.LIFETIME_ON_APPLY && entry.LifetimeStart == TestDrawableHitObject.LIFETIME_ON_APPLY);
|
||||
@ -97,11 +93,9 @@ namespace osu.Game.Tests.Gameplay
|
||||
{
|
||||
TestDrawableHitObject dho = null;
|
||||
TestLifetimeEntry entry = null;
|
||||
AddStep("Create DHO", () =>
|
||||
AddStep("Create DHO", () => Child = dho = new TestDrawableHitObject
|
||||
{
|
||||
dho = new TestDrawableHitObject(null);
|
||||
dho.Apply(entry = new TestLifetimeEntry(new HitObject()));
|
||||
Child = dho;
|
||||
Entry = entry = new TestLifetimeEntry(new HitObject())
|
||||
});
|
||||
|
||||
AddStep("Set entry lifetime", () =>
|
||||
@ -135,7 +129,7 @@ namespace osu.Game.Tests.Gameplay
|
||||
|
||||
public bool SetLifetimeStartOnApply;
|
||||
|
||||
public TestDrawableHitObject(HitObject hitObject)
|
||||
public TestDrawableHitObject(HitObject hitObject = null)
|
||||
: base(hitObject)
|
||||
{
|
||||
}
|
||||
|
@ -21,6 +21,14 @@ namespace osu.Game.Tests.Mods
|
||||
Assert.That(ModUtils.CheckCompatibleSet(new[] { mod.Object }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestModIsCompatibleByItselfWithIncompatibleInterface()
|
||||
{
|
||||
var mod = new Mock<CustomMod1>();
|
||||
mod.Setup(m => m.IncompatibleMods).Returns(new[] { typeof(IModCompatibilitySpecification) });
|
||||
Assert.That(ModUtils.CheckCompatibleSet(new[] { mod.Object }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestIncompatibleThroughTopLevel()
|
||||
{
|
||||
@ -34,6 +42,20 @@ namespace osu.Game.Tests.Mods
|
||||
Assert.That(ModUtils.CheckCompatibleSet(new Mod[] { mod2.Object, mod1.Object }), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestIncompatibleThroughInterface()
|
||||
{
|
||||
var mod1 = new Mock<CustomMod1>();
|
||||
var mod2 = new Mock<CustomMod2>();
|
||||
|
||||
mod1.Setup(m => m.IncompatibleMods).Returns(new[] { typeof(IModCompatibilitySpecification) });
|
||||
mod2.Setup(m => m.IncompatibleMods).Returns(new[] { typeof(IModCompatibilitySpecification) });
|
||||
|
||||
// Test both orderings.
|
||||
Assert.That(ModUtils.CheckCompatibleSet(new Mod[] { mod1.Object, mod2.Object }), Is.False);
|
||||
Assert.That(ModUtils.CheckCompatibleSet(new Mod[] { mod2.Object, mod1.Object }), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMultiModIncompatibleWithTopLevel()
|
||||
{
|
||||
@ -149,11 +171,15 @@ namespace osu.Game.Tests.Mods
|
||||
Assert.That(invalid.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid));
|
||||
}
|
||||
|
||||
public abstract class CustomMod1 : Mod
|
||||
public abstract class CustomMod1 : Mod, IModCompatibilitySpecification
|
||||
{
|
||||
}
|
||||
|
||||
public abstract class CustomMod2 : Mod
|
||||
public abstract class CustomMod2 : Mod, IModCompatibilitySpecification
|
||||
{
|
||||
}
|
||||
|
||||
public interface IModCompatibilitySpecification
|
||||
{
|
||||
}
|
||||
}
|
||||
|
@ -142,7 +142,10 @@ namespace osu.Game.Tests.NonVisual
|
||||
|
||||
foreach (var file in osuStorage.IgnoreFiles)
|
||||
{
|
||||
Assert.That(File.Exists(Path.Combine(originalDirectory, file)));
|
||||
// avoid touching realm files which may be a pipe and break everything.
|
||||
// this is also done locally inside OsuStorage via the IgnoreFiles list.
|
||||
if (file.EndsWith(".ini", StringComparison.Ordinal))
|
||||
Assert.That(File.Exists(Path.Combine(originalDirectory, file)));
|
||||
Assert.That(storage.Exists(file), Is.False);
|
||||
}
|
||||
|
||||
|
@ -66,12 +66,12 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestStoryboardExitToSkipOutro()
|
||||
public void TestStoryboardExitDuringOutroStillExits()
|
||||
{
|
||||
CreateTest(null);
|
||||
AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value);
|
||||
AddStep("exit via pause", () => Player.ExitViaPause());
|
||||
AddAssert("score shown", () => Player.IsScoreShown);
|
||||
AddAssert("player exited", () => !Player.IsCurrentScreen() && Player.GetChildScreen() == null);
|
||||
}
|
||||
|
||||
[TestCase(false)]
|
||||
|
@ -74,7 +74,6 @@ namespace osu.Game.Tests.Visual
|
||||
typeof(FileStore),
|
||||
typeof(ScoreManager),
|
||||
typeof(BeatmapManager),
|
||||
typeof(KeyBindingStore),
|
||||
typeof(SettingsStore),
|
||||
typeof(RulesetConfigCache),
|
||||
typeof(OsuColour),
|
||||
|
@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Lists;
|
||||
@ -66,6 +67,7 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
/// </summary>
|
||||
/// <param name="time">The time to find the difficulty control point at.</param>
|
||||
/// <returns>The difficulty control point.</returns>
|
||||
[NotNull]
|
||||
public DifficultyControlPoint DifficultyPointAt(double time) => binarySearchWithFallback(DifficultyPoints, time, DifficultyControlPoint.DEFAULT);
|
||||
|
||||
/// <summary>
|
||||
@ -73,6 +75,7 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
/// </summary>
|
||||
/// <param name="time">The time to find the effect control point at.</param>
|
||||
/// <returns>The effect control point.</returns>
|
||||
[NotNull]
|
||||
public EffectControlPoint EffectPointAt(double time) => binarySearchWithFallback(EffectPoints, time, EffectControlPoint.DEFAULT);
|
||||
|
||||
/// <summary>
|
||||
@ -80,6 +83,7 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
/// </summary>
|
||||
/// <param name="time">The time to find the sound control point at.</param>
|
||||
/// <returns>The sound control point.</returns>
|
||||
[NotNull]
|
||||
public SampleControlPoint SamplePointAt(double time) => binarySearchWithFallback(SamplePoints, time, SamplePoints.Count > 0 ? SamplePoints[0] : SampleControlPoint.DEFAULT);
|
||||
|
||||
/// <summary>
|
||||
@ -87,6 +91,7 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
/// </summary>
|
||||
/// <param name="time">The time to find the timing control point at.</param>
|
||||
/// <returns>The timing control point.</returns>
|
||||
[NotNull]
|
||||
public TimingControlPoint TimingPointAt(double time) => binarySearchWithFallback(TimingPoints, time, TimingPoints.Count > 0 ? TimingPoints[0] : TimingControlPoint.DEFAULT);
|
||||
|
||||
/// <summary>
|
||||
|
16
osu.Game/Database/IHasGuidPrimaryKey.cs
Normal file
16
osu.Game/Database/IHasGuidPrimaryKey.cs
Normal file
@ -0,0 +1,16 @@
|
||||
// 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 Newtonsoft.Json;
|
||||
using Realms;
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
public interface IHasGuidPrimaryKey
|
||||
{
|
||||
[JsonIgnore]
|
||||
[PrimaryKey]
|
||||
Guid ID { get; set; }
|
||||
}
|
||||
}
|
27
osu.Game/Database/IRealmFactory.cs
Normal file
27
osu.Game/Database/IRealmFactory.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 Realms;
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
public interface IRealmFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// The main realm context, bound to the update thread.
|
||||
/// </summary>
|
||||
Realm Context { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get a fresh context for read usage.
|
||||
/// </summary>
|
||||
RealmContextFactory.RealmUsage GetForRead();
|
||||
|
||||
/// <summary>
|
||||
/// Request a context for write usage.
|
||||
/// This method may block if a write is already active on a different thread.
|
||||
/// </summary>
|
||||
/// <returns>A usage containing a usable context.</returns>
|
||||
RealmContextFactory.RealmWriteUsage GetForWrite();
|
||||
}
|
||||
}
|
@ -24,13 +24,15 @@ namespace osu.Game.Database
|
||||
public DbSet<BeatmapDifficulty> BeatmapDifficulty { get; set; }
|
||||
public DbSet<BeatmapMetadata> BeatmapMetadata { get; set; }
|
||||
public DbSet<BeatmapSetInfo> BeatmapSetInfo { get; set; }
|
||||
public DbSet<DatabasedKeyBinding> DatabasedKeyBinding { get; set; }
|
||||
public DbSet<DatabasedSetting> DatabasedSetting { get; set; }
|
||||
public DbSet<FileInfo> FileInfo { get; set; }
|
||||
public DbSet<RulesetInfo> RulesetInfo { get; set; }
|
||||
public DbSet<SkinInfo> SkinInfo { get; set; }
|
||||
public DbSet<ScoreInfo> ScoreInfo { get; set; }
|
||||
|
||||
// migrated to realm
|
||||
public DbSet<DatabasedKeyBinding> DatabasedKeyBinding { get; set; }
|
||||
|
||||
private readonly string connectionString;
|
||||
|
||||
private static readonly Lazy<OsuDbLoggerFactory> logger = new Lazy<OsuDbLoggerFactory>(() => new OsuDbLoggerFactory());
|
||||
|
208
osu.Game/Database/RealmContextFactory.cs
Normal file
208
osu.Game/Database/RealmContextFactory.cs
Normal file
@ -0,0 +1,208 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Statistics;
|
||||
using osu.Game.Input.Bindings;
|
||||
using Realms;
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
public class RealmContextFactory : Component, IRealmFactory
|
||||
{
|
||||
private readonly Storage storage;
|
||||
|
||||
private const string database_name = @"client";
|
||||
|
||||
private const int schema_version = 6;
|
||||
|
||||
/// <summary>
|
||||
/// Lock object which is held for the duration of a write operation (via <see cref="GetForWrite"/>).
|
||||
/// </summary>
|
||||
private readonly object writeLock = new object();
|
||||
|
||||
private static readonly GlobalStatistic<int> reads = GlobalStatistics.Get<int>("Realm", "Get (Read)");
|
||||
private static readonly GlobalStatistic<int> writes = GlobalStatistics.Get<int>("Realm", "Get (Write)");
|
||||
private static readonly GlobalStatistic<int> refreshes = GlobalStatistics.Get<int>("Realm", "Dirty Refreshes");
|
||||
private static readonly GlobalStatistic<int> contexts_created = GlobalStatistics.Get<int>("Realm", "Contexts (Created)");
|
||||
private static readonly GlobalStatistic<int> pending_writes = GlobalStatistics.Get<int>("Realm", "Pending writes");
|
||||
private static readonly GlobalStatistic<int> active_usages = GlobalStatistics.Get<int>("Realm", "Active usages");
|
||||
|
||||
private readonly ManualResetEventSlim blockingResetEvent = new ManualResetEventSlim(true);
|
||||
|
||||
private Realm context;
|
||||
|
||||
public Realm Context
|
||||
{
|
||||
get
|
||||
{
|
||||
if (IsDisposed)
|
||||
throw new InvalidOperationException($"Attempted to access {nameof(Context)} on a disposed context factory");
|
||||
|
||||
if (context == null)
|
||||
{
|
||||
context = createContext();
|
||||
Logger.Log($"Opened realm \"{context.Config.DatabasePath}\" at version {context.Config.SchemaVersion}");
|
||||
}
|
||||
|
||||
// creating a context will ensure our schema is up-to-date and migrated.
|
||||
|
||||
return context;
|
||||
}
|
||||
}
|
||||
|
||||
public RealmContextFactory(Storage storage)
|
||||
{
|
||||
this.storage = storage;
|
||||
}
|
||||
|
||||
public RealmUsage GetForRead()
|
||||
{
|
||||
reads.Value++;
|
||||
return new RealmUsage(this);
|
||||
}
|
||||
|
||||
public RealmWriteUsage GetForWrite()
|
||||
{
|
||||
writes.Value++;
|
||||
pending_writes.Value++;
|
||||
|
||||
Monitor.Enter(writeLock);
|
||||
|
||||
return new RealmWriteUsage(this);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (context?.Refresh() == true)
|
||||
refreshes.Value++;
|
||||
}
|
||||
|
||||
private Realm createContext()
|
||||
{
|
||||
blockingResetEvent.Wait();
|
||||
|
||||
contexts_created.Value++;
|
||||
|
||||
return Realm.GetInstance(new RealmConfiguration(storage.GetFullPath($"{database_name}.realm", true))
|
||||
{
|
||||
SchemaVersion = schema_version,
|
||||
MigrationCallback = onMigration,
|
||||
});
|
||||
}
|
||||
|
||||
private void onMigration(Migration migration, ulong lastSchemaVersion)
|
||||
{
|
||||
switch (lastSchemaVersion)
|
||||
{
|
||||
case 5:
|
||||
// let's keep things simple. changing the type of the primary key is a bit involved.
|
||||
migration.NewRealm.RemoveAll<RealmKeyBinding>();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
BlockAllOperations();
|
||||
}
|
||||
|
||||
public IDisposable BlockAllOperations()
|
||||
{
|
||||
blockingResetEvent.Reset();
|
||||
flushContexts();
|
||||
|
||||
return new InvokeOnDisposal<RealmContextFactory>(this, r => endBlockingSection());
|
||||
}
|
||||
|
||||
private void endBlockingSection()
|
||||
{
|
||||
blockingResetEvent.Set();
|
||||
}
|
||||
|
||||
private void flushContexts()
|
||||
{
|
||||
var previousContext = context;
|
||||
context = null;
|
||||
|
||||
// wait for all threaded usages to finish
|
||||
while (active_usages.Value > 0)
|
||||
Thread.Sleep(50);
|
||||
|
||||
previousContext?.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A usage of realm from an arbitrary thread.
|
||||
/// </summary>
|
||||
public class RealmUsage : IDisposable
|
||||
{
|
||||
public readonly Realm Realm;
|
||||
|
||||
protected readonly RealmContextFactory Factory;
|
||||
|
||||
internal RealmUsage(RealmContextFactory factory)
|
||||
{
|
||||
active_usages.Value++;
|
||||
Factory = factory;
|
||||
Realm = factory.createContext();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes this instance, calling the initially captured action.
|
||||
/// </summary>
|
||||
public virtual void Dispose()
|
||||
{
|
||||
Realm?.Dispose();
|
||||
active_usages.Value--;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A transaction used for making changes to realm data.
|
||||
/// </summary>
|
||||
public class RealmWriteUsage : RealmUsage
|
||||
{
|
||||
private readonly Transaction transaction;
|
||||
|
||||
internal RealmWriteUsage(RealmContextFactory factory)
|
||||
: base(factory)
|
||||
{
|
||||
transaction = Realm.BeginWrite();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Commit all changes made in this transaction.
|
||||
/// </summary>
|
||||
public void Commit() => transaction.Commit();
|
||||
|
||||
/// <summary>
|
||||
/// Revert all changes made in this transaction.
|
||||
/// </summary>
|
||||
public void Rollback() => transaction.Rollback();
|
||||
|
||||
/// <summary>
|
||||
/// Disposes this instance, calling the initially captured action.
|
||||
/// </summary>
|
||||
public override void Dispose()
|
||||
{
|
||||
// rollback if not explicitly committed.
|
||||
transaction?.Dispose();
|
||||
|
||||
base.Dispose();
|
||||
|
||||
Monitor.Exit(Factory.writeLock);
|
||||
pending_writes.Value--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
51
osu.Game/Database/RealmExtensions.cs
Normal file
51
osu.Game/Database/RealmExtensions.cs
Normal file
@ -0,0 +1,51 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using AutoMapper;
|
||||
using osu.Game.Input.Bindings;
|
||||
using Realms;
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
public static class RealmExtensions
|
||||
{
|
||||
private static readonly IMapper mapper = new MapperConfiguration(c =>
|
||||
{
|
||||
c.ShouldMapField = fi => false;
|
||||
c.ShouldMapProperty = pi => pi.SetMethod != null && pi.SetMethod.IsPublic;
|
||||
|
||||
c.CreateMap<RealmKeyBinding, RealmKeyBinding>();
|
||||
}).CreateMapper();
|
||||
|
||||
/// <summary>
|
||||
/// Create a detached copy of the each item in the collection.
|
||||
/// </summary>
|
||||
/// <param name="items">A list of managed <see cref="RealmObject"/>s to detach.</param>
|
||||
/// <typeparam name="T">The type of object.</typeparam>
|
||||
/// <returns>A list containing non-managed copies of provided items.</returns>
|
||||
public static List<T> Detach<T>(this IEnumerable<T> items) where T : RealmObject
|
||||
{
|
||||
var list = new List<T>();
|
||||
|
||||
foreach (var obj in items)
|
||||
list.Add(obj.Detach());
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a detached copy of the item.
|
||||
/// </summary>
|
||||
/// <param name="item">The managed <see cref="RealmObject"/> to detach.</param>
|
||||
/// <typeparam name="T">The type of object.</typeparam>
|
||||
/// <returns>A non-managed copy of provided item. Will return the provided item if already detached.</returns>
|
||||
public static T Detach<T>(this T item) where T : RealmObject
|
||||
{
|
||||
if (!item.IsManaged)
|
||||
return item;
|
||||
|
||||
return mapper.Map<T>(item);
|
||||
}
|
||||
}
|
||||
}
|
33
osu.Game/Extensions/LanguageExtensions.cs
Normal file
33
osu.Game/Extensions/LanguageExtensions.cs
Normal file
@ -0,0 +1,33 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using osu.Game.Localisation;
|
||||
|
||||
namespace osu.Game.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Conversion utilities for the <see cref="Language"/> enum.
|
||||
/// </summary>
|
||||
public static class LanguageExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the culture code of the <see cref="CultureInfo"/> that corresponds to the supplied <paramref name="language"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is required as enum member names are not allowed to contain hyphens.
|
||||
/// </remarks>
|
||||
public static string ToCultureCode(this Language language)
|
||||
=> language.ToString().Replace("_", "-");
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse the supplied <paramref name="cultureCode"/> to a <see cref="Language"/> value.
|
||||
/// </summary>
|
||||
/// <param name="cultureCode">The code of the culture to parse.</param>
|
||||
/// <param name="language">The parsed <see cref="Language"/>. Valid only if the return value of the method is <see langword="true" />.</param>
|
||||
/// <returns>Whether the parsing succeeded.</returns>
|
||||
public static bool TryParseCultureCode(string cultureCode, out Language language)
|
||||
=> Enum.TryParse(cultureCode.Replace("-", "_"), out language);
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Events;
|
||||
using osuTK.Graphics;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
|
||||
namespace osu.Game.Graphics.Containers
|
||||
{
|
||||
@ -20,7 +21,8 @@ namespace osu.Game.Graphics.Containers
|
||||
|
||||
protected virtual IEnumerable<Drawable> EffectTargets => new[] { Content };
|
||||
|
||||
public OsuHoverContainer()
|
||||
public OsuHoverContainer(HoverSampleSet sampleSet = HoverSampleSet.Default)
|
||||
: base(sampleSet)
|
||||
{
|
||||
Enabled.ValueChanged += e =>
|
||||
{
|
||||
|
@ -13,13 +13,13 @@ namespace osu.Game.Graphics.UserInterface
|
||||
[Description("button")]
|
||||
Button,
|
||||
|
||||
[Description("softer")]
|
||||
Soft,
|
||||
|
||||
[Description("toolbar")]
|
||||
Toolbar,
|
||||
|
||||
[Description("songselect")]
|
||||
SongSelect
|
||||
[Description("tabselect")]
|
||||
TabSelect,
|
||||
|
||||
[Description("scrolltotop")]
|
||||
ScrollToTop
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,8 @@
|
||||
using System.Linq;
|
||||
using osuTK.Graphics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@ -57,6 +59,9 @@ namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
public override bool HandleNonPositionalInput => State == MenuState.Open;
|
||||
|
||||
private Sample sampleOpen;
|
||||
private Sample sampleClose;
|
||||
|
||||
// todo: this uses the same styling as OsuMenu. hopefully we can just use OsuMenu in the future with some refactoring
|
||||
public OsuDropdownMenu()
|
||||
{
|
||||
@ -69,9 +74,30 @@ namespace osu.Game.Graphics.UserInterface
|
||||
ItemsContainer.Padding = new MarginPadding(5);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(AudioManager audio)
|
||||
{
|
||||
sampleOpen = audio.Samples.Get(@"UI/dropdown-open");
|
||||
sampleClose = audio.Samples.Get(@"UI/dropdown-close");
|
||||
}
|
||||
|
||||
// todo: this shouldn't be required after https://github.com/ppy/osu-framework/issues/4519 is fixed.
|
||||
private bool wasOpened;
|
||||
|
||||
// todo: this uses the same styling as OsuMenu. hopefully we can just use OsuMenu in the future with some refactoring
|
||||
protected override void AnimateOpen() => this.FadeIn(300, Easing.OutQuint);
|
||||
protected override void AnimateClose() => this.FadeOut(300, Easing.OutQuint);
|
||||
protected override void AnimateOpen()
|
||||
{
|
||||
wasOpened = true;
|
||||
this.FadeIn(300, Easing.OutQuint);
|
||||
sampleOpen?.Play();
|
||||
}
|
||||
|
||||
protected override void AnimateClose()
|
||||
{
|
||||
this.FadeOut(300, Easing.OutQuint);
|
||||
if (wasOpened)
|
||||
sampleClose?.Play();
|
||||
}
|
||||
|
||||
// todo: this uses the same styling as OsuMenu. hopefully we can just use OsuMenu in the future with some refactoring
|
||||
protected override void UpdateSize(Vector2 newSize)
|
||||
@ -155,7 +181,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
nonAccentSelectedColour = Color4.Black.Opacity(0.5f);
|
||||
updateColours();
|
||||
|
||||
AddInternal(new HoverClickSounds(HoverSampleSet.Soft));
|
||||
AddInternal(new HoverSounds());
|
||||
}
|
||||
|
||||
protected override void UpdateForegroundColour()
|
||||
@ -262,7 +288,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
},
|
||||
};
|
||||
|
||||
AddInternal(new HoverClickSounds());
|
||||
AddInternal(new HoverSounds());
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
|
@ -160,7 +160,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
Margin = new MarginPadding { Top = 5, Bottom = 5 },
|
||||
Origin = Anchor.BottomLeft,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Text = (value as IHasDescription)?.Description ?? (value as Enum)?.GetDescription() ?? value.ToString(),
|
||||
Text = (value as IHasDescription)?.Description ?? (value as Enum)?.GetLocalisableDescription() ?? value.ToString(),
|
||||
Font = OsuFont.GetFont(size: 14)
|
||||
},
|
||||
Bar = new Box
|
||||
@ -172,7 +172,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
Origin = Anchor.BottomLeft,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
},
|
||||
new HoverClickSounds()
|
||||
new HoverClickSounds(HoverSampleSet.TabSelect)
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,8 @@
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
@ -43,6 +45,8 @@ namespace osu.Game.Graphics.UserInterface
|
||||
}
|
||||
|
||||
private const float transition_length = 500;
|
||||
private Sample sampleChecked;
|
||||
private Sample sampleUnchecked;
|
||||
|
||||
public OsuTabControlCheckbox()
|
||||
{
|
||||
@ -77,8 +81,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
Colour = Color4.White,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
},
|
||||
new HoverClickSounds()
|
||||
}
|
||||
};
|
||||
|
||||
Current.ValueChanged += selected =>
|
||||
@ -91,10 +94,13 @@ namespace osu.Game.Graphics.UserInterface
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
private void load(OsuColour colours, AudioManager audio)
|
||||
{
|
||||
if (accentColour == null)
|
||||
AccentColour = colours.Blue;
|
||||
|
||||
sampleChecked = audio.Samples.Get(@"UI/check-on");
|
||||
sampleUnchecked = audio.Samples.Get(@"UI/check-off");
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
@ -111,6 +117,16 @@ namespace osu.Game.Graphics.UserInterface
|
||||
base.OnHoverLost(e);
|
||||
}
|
||||
|
||||
protected override void OnUserChange(bool value)
|
||||
{
|
||||
base.OnUserChange(value);
|
||||
|
||||
if (value)
|
||||
sampleChecked?.Play();
|
||||
else
|
||||
sampleUnchecked?.Play();
|
||||
}
|
||||
|
||||
private void updateFade()
|
||||
{
|
||||
box.FadeTo(Current.Value || IsHovered ? 1 : 0, transition_length, Easing.OutQuint);
|
||||
|
@ -11,6 +11,7 @@ using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterface
|
||||
@ -75,13 +76,13 @@ namespace osu.Game.Graphics.UserInterface
|
||||
Origin = Anchor.BottomLeft,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
},
|
||||
new HoverClickSounds()
|
||||
new HoverClickSounds(HoverSampleSet.TabSelect)
|
||||
};
|
||||
|
||||
Active.BindValueChanged(active => Text.Font = Text.Font.With(Typeface.Torus, weight: active.NewValue ? FontWeight.Bold : FontWeight.Medium), true);
|
||||
}
|
||||
|
||||
protected virtual string CreateText() => (Value as Enum)?.GetDescription() ?? Value.ToString();
|
||||
protected virtual LocalisableString CreateText() => (Value as Enum)?.GetLocalisableDescription() ?? Value.ToString();
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
|
@ -33,12 +33,18 @@ namespace osu.Game.IO
|
||||
private readonly StorageConfigManager storageConfig;
|
||||
private readonly Storage defaultStorage;
|
||||
|
||||
public override string[] IgnoreDirectories => new[] { "cache" };
|
||||
public override string[] IgnoreDirectories => new[]
|
||||
{
|
||||
"cache",
|
||||
"client.realm.management"
|
||||
};
|
||||
|
||||
public override string[] IgnoreFiles => new[]
|
||||
{
|
||||
"framework.ini",
|
||||
"storage.ini"
|
||||
"storage.ini",
|
||||
"client.realm.note",
|
||||
"client.realm.lock",
|
||||
};
|
||||
|
||||
public OsuStorage(GameHost host, Storage defaultStorage)
|
||||
|
@ -8,7 +8,7 @@ using osu.Game.Database;
|
||||
namespace osu.Game.Input.Bindings
|
||||
{
|
||||
[Table("KeyBinding")]
|
||||
public class DatabasedKeyBinding : KeyBinding, IHasPrimaryKey
|
||||
public class DatabasedKeyBinding : IKeyBinding, IHasPrimaryKey
|
||||
{
|
||||
public int ID { get; set; }
|
||||
|
||||
@ -17,17 +17,23 @@ namespace osu.Game.Input.Bindings
|
||||
public int? Variant { get; set; }
|
||||
|
||||
[Column("Keys")]
|
||||
public string KeysString
|
||||
{
|
||||
get => KeyCombination.ToString();
|
||||
private set => KeyCombination = value;
|
||||
}
|
||||
public string KeysString { get; set; }
|
||||
|
||||
[Column("Action")]
|
||||
public int IntAction
|
||||
public int IntAction { get; set; }
|
||||
|
||||
[NotMapped]
|
||||
public KeyCombination KeyCombination
|
||||
{
|
||||
get => (int)Action;
|
||||
set => Action = value;
|
||||
get => KeysString;
|
||||
set => KeysString = value.ToString();
|
||||
}
|
||||
|
||||
[NotMapped]
|
||||
public object Action
|
||||
{
|
||||
get => IntAction;
|
||||
set => IntAction = (int)value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,10 +3,12 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Rulesets;
|
||||
using System.Linq;
|
||||
using Realms;
|
||||
|
||||
namespace osu.Game.Input.Bindings
|
||||
{
|
||||
@ -21,7 +23,11 @@ namespace osu.Game.Input.Bindings
|
||||
|
||||
private readonly int? variant;
|
||||
|
||||
private KeyBindingStore store;
|
||||
private IDisposable realmSubscription;
|
||||
private IQueryable<RealmKeyBinding> realmKeyBindings;
|
||||
|
||||
[Resolved]
|
||||
private RealmContextFactory realmFactory { get; set; }
|
||||
|
||||
public override IEnumerable<IKeyBinding> DefaultKeyBindings => ruleset.CreateInstance().GetDefaultKeyBindings(variant ?? 0);
|
||||
|
||||
@ -42,24 +48,34 @@ namespace osu.Game.Input.Bindings
|
||||
throw new InvalidOperationException($"{nameof(variant)} can not be null when a non-null {nameof(ruleset)} is provided.");
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(KeyBindingStore keyBindings)
|
||||
{
|
||||
store = keyBindings;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
if (ruleset == null || ruleset.ID.HasValue)
|
||||
{
|
||||
var rulesetId = ruleset?.ID;
|
||||
|
||||
realmKeyBindings = realmFactory.Context.All<RealmKeyBinding>()
|
||||
.Where(b => b.RulesetID == rulesetId && b.Variant == variant);
|
||||
|
||||
realmSubscription = realmKeyBindings
|
||||
.SubscribeForNotifications((sender, changes, error) =>
|
||||
{
|
||||
// first subscription ignored as we are handling this in LoadComplete.
|
||||
if (changes == null)
|
||||
return;
|
||||
|
||||
ReloadMappings();
|
||||
});
|
||||
}
|
||||
|
||||
base.LoadComplete();
|
||||
store.KeyBindingChanged += ReloadMappings;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (store != null)
|
||||
store.KeyBindingChanged -= ReloadMappings;
|
||||
realmSubscription?.Dispose();
|
||||
}
|
||||
|
||||
protected override void ReloadMappings()
|
||||
@ -67,17 +83,17 @@ namespace osu.Game.Input.Bindings
|
||||
var defaults = DefaultKeyBindings.ToList();
|
||||
|
||||
if (ruleset != null && !ruleset.ID.HasValue)
|
||||
// if the provided ruleset is not stored to the database, we have no way to retrieve custom bindings.
|
||||
// fallback to defaults instead.
|
||||
// some tests instantiate a ruleset which is not present in the database.
|
||||
// in these cases we still want key bindings to work, but matching to database instances would result in none being present,
|
||||
// so let's populate the defaults directly.
|
||||
KeyBindings = defaults;
|
||||
else
|
||||
{
|
||||
KeyBindings = store.Query(ruleset?.ID, variant)
|
||||
.OrderBy(b => defaults.FindIndex(d => (int)d.Action == b.IntAction))
|
||||
// this ordering is important to ensure that we read entries from the database in the order
|
||||
// enforced by DefaultKeyBindings. allow for song select to handle actions that may otherwise
|
||||
// have been eaten by the music controller due to query order.
|
||||
.ToList();
|
||||
KeyBindings = realmKeyBindings.Detach()
|
||||
// this ordering is important to ensure that we read entries from the database in the order
|
||||
// enforced by DefaultKeyBindings. allow for song select to handle actions that may otherwise
|
||||
// have been eaten by the music controller due to query order.
|
||||
.OrderBy(b => defaults.FindIndex(d => (int)d.Action == b.ActionInt)).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
39
osu.Game/Input/Bindings/RealmKeyBinding.cs
Normal file
39
osu.Game/Input/Bindings/RealmKeyBinding.cs
Normal file
@ -0,0 +1,39 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Game.Database;
|
||||
using Realms;
|
||||
|
||||
namespace osu.Game.Input.Bindings
|
||||
{
|
||||
[MapTo(nameof(KeyBinding))]
|
||||
public class RealmKeyBinding : RealmObject, IHasGuidPrimaryKey, IKeyBinding
|
||||
{
|
||||
[PrimaryKey]
|
||||
public Guid ID { get; set; } = Guid.NewGuid();
|
||||
|
||||
public int? RulesetID { get; set; }
|
||||
|
||||
public int? Variant { get; set; }
|
||||
|
||||
public KeyCombination KeyCombination
|
||||
{
|
||||
get => KeyCombinationString;
|
||||
set => KeyCombinationString = value.ToString();
|
||||
}
|
||||
|
||||
public object Action
|
||||
{
|
||||
get => ActionInt;
|
||||
set => ActionInt = (int)value;
|
||||
}
|
||||
|
||||
[MapTo(nameof(Action))]
|
||||
public int ActionInt { get; set; }
|
||||
|
||||
[MapTo(nameof(KeyCombination))]
|
||||
public string KeyCombinationString { get; set; }
|
||||
}
|
||||
}
|
@ -1,133 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Rulesets;
|
||||
|
||||
namespace osu.Game.Input
|
||||
{
|
||||
public class KeyBindingStore : DatabaseBackedStore
|
||||
{
|
||||
public event Action KeyBindingChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Keys which should not be allowed for gameplay input purposes.
|
||||
/// </summary>
|
||||
private static readonly IEnumerable<InputKey> banned_keys = new[]
|
||||
{
|
||||
InputKey.MouseWheelDown,
|
||||
InputKey.MouseWheelLeft,
|
||||
InputKey.MouseWheelUp,
|
||||
InputKey.MouseWheelRight
|
||||
};
|
||||
|
||||
public KeyBindingStore(DatabaseContextFactory contextFactory, RulesetStore rulesets, Storage storage = null)
|
||||
: base(contextFactory, storage)
|
||||
{
|
||||
using (ContextFactory.GetForWrite())
|
||||
{
|
||||
foreach (var info in rulesets.AvailableRulesets)
|
||||
{
|
||||
var ruleset = info.CreateInstance();
|
||||
foreach (var variant in ruleset.AvailableVariants)
|
||||
insertDefaults(ruleset.GetDefaultKeyBindings(variant), info.ID, variant);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Register(KeyBindingContainer manager) => insertDefaults(manager.DefaultKeyBindings);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve all user-defined key combinations (in a format that can be displayed) for a specific action.
|
||||
/// </summary>
|
||||
/// <param name="globalAction">The action to lookup.</param>
|
||||
/// <returns>A set of display strings for all the user's key configuration for the action.</returns>
|
||||
public IEnumerable<string> GetReadableKeyCombinationsFor(GlobalAction globalAction)
|
||||
{
|
||||
foreach (var action in Query().Where(b => (GlobalAction)b.Action == globalAction))
|
||||
{
|
||||
string str = action.KeyCombination.ReadableString();
|
||||
|
||||
// even if found, the readable string may be empty for an unbound action.
|
||||
if (str.Length > 0)
|
||||
yield return str;
|
||||
}
|
||||
}
|
||||
|
||||
private void insertDefaults(IEnumerable<IKeyBinding> defaults, int? rulesetId = null, int? variant = null)
|
||||
{
|
||||
using (var usage = ContextFactory.GetForWrite())
|
||||
{
|
||||
// compare counts in database vs defaults
|
||||
foreach (var group in defaults.GroupBy(k => k.Action))
|
||||
{
|
||||
int count = Query(rulesetId, variant).Count(k => (int)k.Action == (int)group.Key);
|
||||
int aimCount = group.Count();
|
||||
|
||||
if (aimCount <= count)
|
||||
continue;
|
||||
|
||||
foreach (var insertable in group.Skip(count).Take(aimCount - count))
|
||||
{
|
||||
// insert any defaults which are missing.
|
||||
usage.Context.DatabasedKeyBinding.Add(new DatabasedKeyBinding
|
||||
{
|
||||
KeyCombination = insertable.KeyCombination,
|
||||
Action = insertable.Action,
|
||||
RulesetID = rulesetId,
|
||||
Variant = variant
|
||||
});
|
||||
|
||||
// required to ensure stable insert order (https://github.com/dotnet/efcore/issues/11686)
|
||||
usage.Context.SaveChanges();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve <see cref="DatabasedKeyBinding"/>s for a specified ruleset/variant content.
|
||||
/// </summary>
|
||||
/// <param name="rulesetId">The ruleset's internal ID.</param>
|
||||
/// <param name="variant">An optional variant.</param>
|
||||
public List<DatabasedKeyBinding> Query(int? rulesetId = null, int? variant = null) =>
|
||||
ContextFactory.Get().DatabasedKeyBinding.Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList();
|
||||
|
||||
public void Update(KeyBinding keyBinding)
|
||||
{
|
||||
using (ContextFactory.GetForWrite())
|
||||
{
|
||||
var dbKeyBinding = (DatabasedKeyBinding)keyBinding;
|
||||
|
||||
Debug.Assert(dbKeyBinding.RulesetID == null || CheckValidForGameplay(keyBinding.KeyCombination));
|
||||
|
||||
Refresh(ref dbKeyBinding);
|
||||
|
||||
if (dbKeyBinding.KeyCombination.Equals(keyBinding.KeyCombination))
|
||||
return;
|
||||
|
||||
dbKeyBinding.KeyCombination = keyBinding.KeyCombination;
|
||||
}
|
||||
|
||||
KeyBindingChanged?.Invoke();
|
||||
}
|
||||
|
||||
public static bool CheckValidForGameplay(KeyCombination combination)
|
||||
{
|
||||
foreach (var key in banned_keys)
|
||||
{
|
||||
if (combination.Keys.Contains(key))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
117
osu.Game/Input/RealmKeyBindingStore.cs
Normal file
117
osu.Game/Input/RealmKeyBindingStore.cs
Normal file
@ -0,0 +1,117 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Rulesets;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace osu.Game.Input
|
||||
{
|
||||
public class RealmKeyBindingStore
|
||||
{
|
||||
private readonly RealmContextFactory realmFactory;
|
||||
|
||||
public RealmKeyBindingStore(RealmContextFactory realmFactory)
|
||||
{
|
||||
this.realmFactory = realmFactory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve all user-defined key combinations (in a format that can be displayed) for a specific action.
|
||||
/// </summary>
|
||||
/// <param name="globalAction">The action to lookup.</param>
|
||||
/// <returns>A set of display strings for all the user's key configuration for the action.</returns>
|
||||
public IReadOnlyList<string> GetReadableKeyCombinationsFor(GlobalAction globalAction)
|
||||
{
|
||||
List<string> combinations = new List<string>();
|
||||
|
||||
using (var context = realmFactory.GetForRead())
|
||||
{
|
||||
foreach (var action in context.Realm.All<RealmKeyBinding>().Where(b => b.RulesetID == null && (GlobalAction)b.ActionInt == globalAction))
|
||||
{
|
||||
string str = action.KeyCombination.ReadableString();
|
||||
|
||||
// even if found, the readable string may be empty for an unbound action.
|
||||
if (str.Length > 0)
|
||||
combinations.Add(str);
|
||||
}
|
||||
}
|
||||
|
||||
return combinations;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register a new type of <see cref="KeyBindingContainer{T}"/>, adding default bindings from <see cref="KeyBindingContainer.DefaultKeyBindings"/>.
|
||||
/// </summary>
|
||||
/// <param name="container">The container to populate defaults from.</param>
|
||||
public void Register(KeyBindingContainer container) => insertDefaults(container.DefaultKeyBindings);
|
||||
|
||||
/// <summary>
|
||||
/// Register a ruleset, adding default bindings for each of its variants.
|
||||
/// </summary>
|
||||
/// <param name="ruleset">The ruleset to populate defaults from.</param>
|
||||
public void Register(RulesetInfo ruleset)
|
||||
{
|
||||
var instance = ruleset.CreateInstance();
|
||||
|
||||
foreach (var variant in instance.AvailableVariants)
|
||||
insertDefaults(instance.GetDefaultKeyBindings(variant), ruleset.ID, variant);
|
||||
}
|
||||
|
||||
private void insertDefaults(IEnumerable<IKeyBinding> defaults, int? rulesetId = null, int? variant = null)
|
||||
{
|
||||
using (var usage = realmFactory.GetForWrite())
|
||||
{
|
||||
// compare counts in database vs defaults
|
||||
foreach (var defaultsForAction in defaults.GroupBy(k => k.Action))
|
||||
{
|
||||
int existingCount = usage.Realm.All<RealmKeyBinding>().Count(k => k.RulesetID == rulesetId && k.Variant == variant && k.ActionInt == (int)defaultsForAction.Key);
|
||||
|
||||
if (defaultsForAction.Count() <= existingCount)
|
||||
continue;
|
||||
|
||||
foreach (var k in defaultsForAction.Skip(existingCount))
|
||||
{
|
||||
// insert any defaults which are missing.
|
||||
usage.Realm.Add(new RealmKeyBinding
|
||||
{
|
||||
KeyCombinationString = k.KeyCombination.ToString(),
|
||||
ActionInt = (int)k.Action,
|
||||
RulesetID = rulesetId,
|
||||
Variant = variant
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
usage.Commit();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Keys which should not be allowed for gameplay input purposes.
|
||||
/// </summary>
|
||||
private static readonly IEnumerable<InputKey> banned_keys = new[]
|
||||
{
|
||||
InputKey.MouseWheelDown,
|
||||
InputKey.MouseWheelLeft,
|
||||
InputKey.MouseWheelUp,
|
||||
InputKey.MouseWheelRight
|
||||
};
|
||||
|
||||
public static bool CheckValidForGameplay(KeyCombination combination)
|
||||
{
|
||||
foreach (var key in banned_keys)
|
||||
{
|
||||
if (combination.Keys.Contains(key))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
@ -10,7 +10,103 @@ namespace osu.Game.Localisation
|
||||
[Description(@"English")]
|
||||
en,
|
||||
|
||||
// TODO: Requires Arabic glyphs to be added to resources (and possibly also RTL support).
|
||||
// [Description(@"اَلْعَرَبِيَّةُ")]
|
||||
// ar,
|
||||
|
||||
[Description(@"Беларуская мова")]
|
||||
be,
|
||||
|
||||
[Description(@"Български")]
|
||||
bg,
|
||||
|
||||
[Description(@"Česky")]
|
||||
cs,
|
||||
|
||||
[Description(@"Dansk")]
|
||||
da,
|
||||
|
||||
[Description(@"Deutsch")]
|
||||
de,
|
||||
|
||||
[Description(@"Ελληνικά")]
|
||||
el,
|
||||
|
||||
[Description(@"español")]
|
||||
es,
|
||||
|
||||
[Description(@"Suomi")]
|
||||
fi,
|
||||
|
||||
[Description(@"français")]
|
||||
fr,
|
||||
|
||||
[Description(@"Magyar")]
|
||||
hu,
|
||||
|
||||
[Description(@"Bahasa Indonesia")]
|
||||
id,
|
||||
|
||||
[Description(@"Italiano")]
|
||||
it,
|
||||
|
||||
[Description(@"日本語")]
|
||||
ja
|
||||
ja,
|
||||
|
||||
[Description(@"한국어")]
|
||||
ko,
|
||||
|
||||
[Description(@"Nederlands")]
|
||||
nl,
|
||||
|
||||
[Description(@"Norsk")]
|
||||
no,
|
||||
|
||||
[Description(@"polski")]
|
||||
pl,
|
||||
|
||||
[Description(@"Português")]
|
||||
pt,
|
||||
|
||||
[Description(@"Português (Brasil)")]
|
||||
pt_br,
|
||||
|
||||
[Description(@"Română")]
|
||||
ro,
|
||||
|
||||
[Description(@"Русский")]
|
||||
ru,
|
||||
|
||||
[Description(@"Slovenčina")]
|
||||
sk,
|
||||
|
||||
[Description(@"Svenska")]
|
||||
sv,
|
||||
|
||||
[Description(@"ไทย")]
|
||||
th,
|
||||
|
||||
// Tagalog has no associated localisations yet, and is not supported on Xamarin platforms or Windows versions <10.
|
||||
// Can be revisited if localisations ever arrive.
|
||||
//[Description(@"Tagalog")]
|
||||
//tl,
|
||||
|
||||
[Description(@"Türkçe")]
|
||||
tr,
|
||||
|
||||
[Description(@"Українська мова")]
|
||||
uk,
|
||||
|
||||
[Description(@"Tiếng Việt")]
|
||||
vi,
|
||||
|
||||
[Description(@"简体中文")]
|
||||
zh,
|
||||
|
||||
[Description(@"繁體中文(香港)")]
|
||||
zh_hk,
|
||||
|
||||
[Description(@"繁體中文(台灣)")]
|
||||
zh_tw
|
||||
}
|
||||
}
|
||||
|
@ -154,6 +154,10 @@ namespace osu.Game.Online.Chat
|
||||
case "beatmapsets":
|
||||
case "d":
|
||||
{
|
||||
if (mainArg == "discussions")
|
||||
// handle discussion links externally for now
|
||||
return new LinkDetails(LinkAction.External, url);
|
||||
|
||||
if (args.Length > 4 && int.TryParse(args[4], out var id))
|
||||
// https://osu.ppy.sh/beatmapsets/1154158#osu/2768184
|
||||
return new LinkDetails(LinkAction.OpenBeatmap, id.ToString());
|
||||
|
@ -6,7 +6,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using MessagePack;
|
||||
using osu.Game.Online.API;
|
||||
|
||||
@ -28,11 +27,9 @@ namespace osu.Game.Online.Multiplayer
|
||||
[Key(3)]
|
||||
public string Name { get; set; } = "Unnamed room";
|
||||
|
||||
[NotNull]
|
||||
[Key(4)]
|
||||
public IEnumerable<APIMod> RequiredMods { get; set; } = Enumerable.Empty<APIMod>();
|
||||
|
||||
[NotNull]
|
||||
[Key(5)]
|
||||
public IEnumerable<APIMod> AllowedMods { get; set; } = Enumerable.Empty<APIMod>();
|
||||
|
||||
|
@ -6,7 +6,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using MessagePack;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Game.Online.API;
|
||||
@ -35,7 +34,6 @@ namespace osu.Game.Online.Multiplayer
|
||||
/// Any mods applicable only to the local user.
|
||||
/// </summary>
|
||||
[Key(3)]
|
||||
[NotNull]
|
||||
public IEnumerable<APIMod> Mods { get; set; } = Enumerable.Empty<APIMod>();
|
||||
|
||||
[IgnoreMember]
|
||||
|
@ -50,8 +50,10 @@ using osu.Game.Updater;
|
||||
using osu.Game.Utils;
|
||||
using LogLevel = osu.Framework.Logging.LogLevel;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Performance;
|
||||
using osu.Game.Skinning.Editor;
|
||||
|
||||
namespace osu.Game
|
||||
@ -487,6 +489,8 @@ namespace osu.Game
|
||||
|
||||
protected virtual UpdateManager CreateUpdateManager() => new UpdateManager();
|
||||
|
||||
protected virtual HighPerformanceSession CreateHighPerformanceSession() => new HighPerformanceSession();
|
||||
|
||||
protected override Container CreateScalingContainer() => new ScalingContainer(ScalingMode.Everything);
|
||||
|
||||
#region Beatmap progression
|
||||
@ -580,8 +584,16 @@ namespace osu.Game
|
||||
|
||||
foreach (var language in Enum.GetValues(typeof(Language)).OfType<Language>())
|
||||
{
|
||||
var cultureCode = language.ToString();
|
||||
Localisation.AddLanguage(cultureCode, new ResourceManagerLocalisationStore(cultureCode));
|
||||
var cultureCode = language.ToCultureCode();
|
||||
|
||||
try
|
||||
{
|
||||
Localisation.AddLanguage(cultureCode, new ResourceManagerLocalisationStore(cultureCode));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error(ex, $"Could not load localisations for language \"{cultureCode}\"");
|
||||
}
|
||||
}
|
||||
|
||||
// The next time this is updated is in UpdateAfterChildren, which occurs too late and results
|
||||
@ -604,9 +616,9 @@ namespace osu.Game
|
||||
|
||||
LocalConfig.LookupKeyBindings = l =>
|
||||
{
|
||||
var combinations = KeyBindingStore.GetReadableKeyCombinationsFor(l).ToArray();
|
||||
var combinations = KeyBindingStore.GetReadableKeyCombinationsFor(l);
|
||||
|
||||
if (combinations.Length == 0)
|
||||
if (combinations.Count == 0)
|
||||
return "none";
|
||||
|
||||
return string.Join(" or ", combinations);
|
||||
@ -755,6 +767,8 @@ namespace osu.Game
|
||||
loadComponentSingleFile(new AccountCreationOverlay(), topMostOverlayContent.Add, true);
|
||||
loadComponentSingleFile(new DialogOverlay(), topMostOverlayContent.Add, true);
|
||||
|
||||
loadComponentSingleFile(CreateHighPerformanceSession(), Add);
|
||||
|
||||
chatOverlay.State.ValueChanged += state => channelManager.HighPollRate.Value = state.NewValue == Visibility.Visible;
|
||||
|
||||
Add(difficultyRecommender);
|
||||
|
@ -95,7 +95,7 @@ namespace osu.Game
|
||||
|
||||
protected RulesetStore RulesetStore { get; private set; }
|
||||
|
||||
protected KeyBindingStore KeyBindingStore { get; private set; }
|
||||
protected RealmKeyBindingStore KeyBindingStore { get; private set; }
|
||||
|
||||
protected MenuCursorContainer MenuCursorContainer { get; private set; }
|
||||
|
||||
@ -144,6 +144,8 @@ namespace osu.Game
|
||||
|
||||
private DatabaseContextFactory contextFactory;
|
||||
|
||||
private RealmContextFactory realmFactory;
|
||||
|
||||
protected override Container<Drawable> Content => content;
|
||||
|
||||
private Container content;
|
||||
@ -179,6 +181,9 @@ namespace osu.Game
|
||||
|
||||
dependencies.Cache(contextFactory = new DatabaseContextFactory(Storage));
|
||||
|
||||
dependencies.Cache(realmFactory = new RealmContextFactory(Storage));
|
||||
AddInternal(realmFactory);
|
||||
|
||||
dependencies.CacheAs(Storage);
|
||||
|
||||
var largeStore = new LargeTextureStore(Host.CreateTextureLoaderStore(new NamespacedResourceStore<byte[]>(Resources, @"Textures")));
|
||||
@ -190,20 +195,29 @@ namespace osu.Game
|
||||
|
||||
AddFont(Resources, @"Fonts/osuFont");
|
||||
|
||||
AddFont(Resources, @"Fonts/Torus-Regular");
|
||||
AddFont(Resources, @"Fonts/Torus-Light");
|
||||
AddFont(Resources, @"Fonts/Torus-SemiBold");
|
||||
AddFont(Resources, @"Fonts/Torus-Bold");
|
||||
AddFont(Resources, @"Fonts/Torus/Torus-Regular");
|
||||
AddFont(Resources, @"Fonts/Torus/Torus-Light");
|
||||
AddFont(Resources, @"Fonts/Torus/Torus-SemiBold");
|
||||
AddFont(Resources, @"Fonts/Torus/Torus-Bold");
|
||||
|
||||
AddFont(Resources, @"Fonts/Noto-Basic");
|
||||
AddFont(Resources, @"Fonts/Noto-Hangul");
|
||||
AddFont(Resources, @"Fonts/Noto-CJK-Basic");
|
||||
AddFont(Resources, @"Fonts/Noto-CJK-Compatibility");
|
||||
AddFont(Resources, @"Fonts/Noto-Thai");
|
||||
AddFont(Resources, @"Fonts/Inter/Inter-Regular");
|
||||
AddFont(Resources, @"Fonts/Inter/Inter-RegularItalic");
|
||||
AddFont(Resources, @"Fonts/Inter/Inter-Light");
|
||||
AddFont(Resources, @"Fonts/Inter/Inter-LightItalic");
|
||||
AddFont(Resources, @"Fonts/Inter/Inter-SemiBold");
|
||||
AddFont(Resources, @"Fonts/Inter/Inter-SemiBoldItalic");
|
||||
AddFont(Resources, @"Fonts/Inter/Inter-Bold");
|
||||
AddFont(Resources, @"Fonts/Inter/Inter-BoldItalic");
|
||||
|
||||
AddFont(Resources, @"Fonts/Venera-Light");
|
||||
AddFont(Resources, @"Fonts/Venera-Bold");
|
||||
AddFont(Resources, @"Fonts/Venera-Black");
|
||||
AddFont(Resources, @"Fonts/Noto/Noto-Basic");
|
||||
AddFont(Resources, @"Fonts/Noto/Noto-Hangul");
|
||||
AddFont(Resources, @"Fonts/Noto/Noto-CJK-Basic");
|
||||
AddFont(Resources, @"Fonts/Noto/Noto-CJK-Compatibility");
|
||||
AddFont(Resources, @"Fonts/Noto/Noto-Thai");
|
||||
|
||||
AddFont(Resources, @"Fonts/Venera/Venera-Light");
|
||||
AddFont(Resources, @"Fonts/Venera/Venera-Bold");
|
||||
AddFont(Resources, @"Fonts/Venera/Venera-Black");
|
||||
|
||||
Audio.Samples.PlaybackConcurrency = SAMPLE_CONCURRENCY;
|
||||
|
||||
@ -275,7 +289,8 @@ namespace osu.Game
|
||||
dependencies.Cache(scorePerformanceManager);
|
||||
AddInternal(scorePerformanceManager);
|
||||
|
||||
dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore));
|
||||
migrateDataToRealm();
|
||||
|
||||
dependencies.Cache(settingsStore = new SettingsStore(contextFactory));
|
||||
dependencies.Cache(rulesetConfigCache = new RulesetConfigCache(settingsStore));
|
||||
|
||||
@ -323,7 +338,12 @@ namespace osu.Game
|
||||
|
||||
base.Content.Add(CreateScalingContainer().WithChildren(mainContent));
|
||||
|
||||
KeyBindingStore = new RealmKeyBindingStore(realmFactory);
|
||||
KeyBindingStore.Register(globalBindings);
|
||||
|
||||
foreach (var r in RulesetStore.AvailableRulesets)
|
||||
KeyBindingStore.Register(r);
|
||||
|
||||
dependencies.Cache(globalBindings);
|
||||
|
||||
PreviewTrackManager previewTrackManager;
|
||||
@ -378,8 +398,11 @@ namespace osu.Game
|
||||
|
||||
public void Migrate(string path)
|
||||
{
|
||||
contextFactory.FlushConnections();
|
||||
(Storage as OsuStorage)?.Migrate(Host.GetStorage(path));
|
||||
using (realmFactory.BlockAllOperations())
|
||||
{
|
||||
contextFactory.FlushConnections();
|
||||
(Storage as OsuStorage)?.Migrate(Host.GetStorage(path));
|
||||
}
|
||||
}
|
||||
|
||||
protected override UserInputManager CreateUserInputManager() => new OsuUserInputManager();
|
||||
@ -390,6 +413,34 @@ namespace osu.Game
|
||||
|
||||
protected override Storage CreateStorage(GameHost host, Storage defaultStorage) => new OsuStorage(host, defaultStorage);
|
||||
|
||||
private void migrateDataToRealm()
|
||||
{
|
||||
using (var db = contextFactory.GetForWrite())
|
||||
using (var usage = realmFactory.GetForWrite())
|
||||
{
|
||||
var existingBindings = db.Context.DatabasedKeyBinding;
|
||||
|
||||
// only migrate data if the realm database is empty.
|
||||
if (!usage.Realm.All<RealmKeyBinding>().Any())
|
||||
{
|
||||
foreach (var dkb in existingBindings)
|
||||
{
|
||||
usage.Realm.Add(new RealmKeyBinding
|
||||
{
|
||||
KeyCombinationString = dkb.KeyCombination.ToString(),
|
||||
ActionInt = (int)dkb.Action,
|
||||
RulesetID = dkb.RulesetID,
|
||||
Variant = dkb.Variant
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
db.Context.RemoveRange(existingBindings);
|
||||
|
||||
usage.Commit();
|
||||
}
|
||||
}
|
||||
|
||||
private void onRulesetChanged(ValueChangedEvent<RulesetInfo> r)
|
||||
{
|
||||
var dict = new Dictionary<ModType, IReadOnlyList<Mod>>();
|
||||
|
@ -1,9 +1,9 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Scoring;
|
||||
@ -33,38 +33,7 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
{
|
||||
}
|
||||
|
||||
protected override LocalisableString LabelFor(ScoreRank value)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case ScoreRank.XH:
|
||||
return BeatmapsStrings.RankXH;
|
||||
|
||||
case ScoreRank.X:
|
||||
return BeatmapsStrings.RankX;
|
||||
|
||||
case ScoreRank.SH:
|
||||
return BeatmapsStrings.RankSH;
|
||||
|
||||
case ScoreRank.S:
|
||||
return BeatmapsStrings.RankS;
|
||||
|
||||
case ScoreRank.A:
|
||||
return BeatmapsStrings.RankA;
|
||||
|
||||
case ScoreRank.B:
|
||||
return BeatmapsStrings.RankB;
|
||||
|
||||
case ScoreRank.C:
|
||||
return BeatmapsStrings.RankC;
|
||||
|
||||
case ScoreRank.D:
|
||||
return BeatmapsStrings.RankD;
|
||||
|
||||
default:
|
||||
throw new ArgumentException("Unsupported value.", nameof(value));
|
||||
}
|
||||
}
|
||||
protected override LocalisableString LabelFor(ScoreRank value) => value.GetLocalisableDescription();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -67,7 +67,7 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
/// <summary>
|
||||
/// Returns the label text to be used for the supplied <paramref name="value"/>.
|
||||
/// </summary>
|
||||
protected virtual LocalisableString LabelFor(T value) => (value as Enum)?.GetDescription() ?? value.ToString();
|
||||
protected virtual LocalisableString LabelFor(T value) => (value as Enum)?.GetLocalisableDescription() ?? value.ToString();
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
|
@ -1,10 +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 System;
|
||||
using System.ComponentModel;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
|
||||
namespace osu.Game.Overlays.BeatmapListing
|
||||
{
|
||||
[LocalisableEnum(typeof(SearchCategoryEnumLocalisationMapper))]
|
||||
public enum SearchCategory
|
||||
{
|
||||
Any,
|
||||
@ -23,4 +27,43 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
[Description("My Maps")]
|
||||
Mine,
|
||||
}
|
||||
|
||||
public class SearchCategoryEnumLocalisationMapper : EnumLocalisationMapper<SearchCategory>
|
||||
{
|
||||
public override LocalisableString Map(SearchCategory value)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case SearchCategory.Any:
|
||||
return BeatmapsStrings.StatusAny;
|
||||
|
||||
case SearchCategory.Leaderboard:
|
||||
return BeatmapsStrings.StatusLeaderboard;
|
||||
|
||||
case SearchCategory.Ranked:
|
||||
return BeatmapsStrings.StatusRanked;
|
||||
|
||||
case SearchCategory.Qualified:
|
||||
return BeatmapsStrings.StatusQualified;
|
||||
|
||||
case SearchCategory.Loved:
|
||||
return BeatmapsStrings.StatusLoved;
|
||||
|
||||
case SearchCategory.Favourites:
|
||||
return BeatmapsStrings.StatusFavourites;
|
||||
|
||||
case SearchCategory.Pending:
|
||||
return BeatmapsStrings.StatusPending;
|
||||
|
||||
case SearchCategory.Graveyard:
|
||||
return BeatmapsStrings.StatusGraveyard;
|
||||
|
||||
case SearchCategory.Mine:
|
||||
return BeatmapsStrings.StatusMine;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(value), value, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,34 @@
|
||||
// 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.Localisation;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
|
||||
namespace osu.Game.Overlays.BeatmapListing
|
||||
{
|
||||
[LocalisableEnum(typeof(SearchExplicitEnumLocalisationMapper))]
|
||||
public enum SearchExplicit
|
||||
{
|
||||
Hide,
|
||||
Show
|
||||
}
|
||||
|
||||
public class SearchExplicitEnumLocalisationMapper : EnumLocalisationMapper<SearchExplicit>
|
||||
{
|
||||
public override LocalisableString Map(SearchExplicit value)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case SearchExplicit.Hide:
|
||||
return BeatmapsStrings.NsfwExclude;
|
||||
|
||||
case SearchExplicit.Show:
|
||||
return BeatmapsStrings.NsfwInclude;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(value), value, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +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 System;
|
||||
using System.ComponentModel;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
|
||||
namespace osu.Game.Overlays.BeatmapListing
|
||||
{
|
||||
[LocalisableEnum(typeof(SearchExtraEnumLocalisationMapper))]
|
||||
public enum SearchExtra
|
||||
{
|
||||
[Description("Has Video")]
|
||||
@ -13,4 +17,22 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
[Description("Has Storyboard")]
|
||||
Storyboard
|
||||
}
|
||||
|
||||
public class SearchExtraEnumLocalisationMapper : EnumLocalisationMapper<SearchExtra>
|
||||
{
|
||||
public override LocalisableString Map(SearchExtra value)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case SearchExtra.Video:
|
||||
return BeatmapsStrings.ExtraVideo;
|
||||
|
||||
case SearchExtra.Storyboard:
|
||||
return BeatmapsStrings.ExtraStoryboard;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(value), value, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +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 System;
|
||||
using System.ComponentModel;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
|
||||
namespace osu.Game.Overlays.BeatmapListing
|
||||
{
|
||||
[LocalisableEnum(typeof(SearchGeneralEnumLocalisationMapper))]
|
||||
public enum SearchGeneral
|
||||
{
|
||||
[Description("Recommended difficulty")]
|
||||
@ -16,4 +20,25 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
[Description("Subscribed mappers")]
|
||||
Follows
|
||||
}
|
||||
|
||||
public class SearchGeneralEnumLocalisationMapper : EnumLocalisationMapper<SearchGeneral>
|
||||
{
|
||||
public override LocalisableString Map(SearchGeneral value)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case SearchGeneral.Recommended:
|
||||
return BeatmapsStrings.GeneralRecommended;
|
||||
|
||||
case SearchGeneral.Converts:
|
||||
return BeatmapsStrings.GeneralConverts;
|
||||
|
||||
case SearchGeneral.Follows:
|
||||
return BeatmapsStrings.GeneralFollows;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(value), value, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +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 System;
|
||||
using System.ComponentModel;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
|
||||
namespace osu.Game.Overlays.BeatmapListing
|
||||
{
|
||||
[LocalisableEnum(typeof(SearchGenreEnumLocalisationMapper))]
|
||||
public enum SearchGenre
|
||||
{
|
||||
Any = 0,
|
||||
@ -26,4 +30,58 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
Folk = 13,
|
||||
Jazz = 14
|
||||
}
|
||||
|
||||
public class SearchGenreEnumLocalisationMapper : EnumLocalisationMapper<SearchGenre>
|
||||
{
|
||||
public override LocalisableString Map(SearchGenre value)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case SearchGenre.Any:
|
||||
return BeatmapsStrings.GenreAny;
|
||||
|
||||
case SearchGenre.Unspecified:
|
||||
return BeatmapsStrings.GenreUnspecified;
|
||||
|
||||
case SearchGenre.VideoGame:
|
||||
return BeatmapsStrings.GenreVideoGame;
|
||||
|
||||
case SearchGenre.Anime:
|
||||
return BeatmapsStrings.GenreAnime;
|
||||
|
||||
case SearchGenre.Rock:
|
||||
return BeatmapsStrings.GenreRock;
|
||||
|
||||
case SearchGenre.Pop:
|
||||
return BeatmapsStrings.GenrePop;
|
||||
|
||||
case SearchGenre.Other:
|
||||
return BeatmapsStrings.GenreOther;
|
||||
|
||||
case SearchGenre.Novelty:
|
||||
return BeatmapsStrings.GenreNovelty;
|
||||
|
||||
case SearchGenre.HipHop:
|
||||
return BeatmapsStrings.GenreHipHop;
|
||||
|
||||
case SearchGenre.Electronic:
|
||||
return BeatmapsStrings.GenreElectronic;
|
||||
|
||||
case SearchGenre.Metal:
|
||||
return BeatmapsStrings.GenreMetal;
|
||||
|
||||
case SearchGenre.Classical:
|
||||
return BeatmapsStrings.GenreClassical;
|
||||
|
||||
case SearchGenre.Folk:
|
||||
return BeatmapsStrings.GenreFolk;
|
||||
|
||||
case SearchGenre.Jazz:
|
||||
return BeatmapsStrings.GenreJazz;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(value), value, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +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 System;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
|
||||
namespace osu.Game.Overlays.BeatmapListing
|
||||
{
|
||||
[LocalisableEnum(typeof(SearchLanguageEnumLocalisationMapper))]
|
||||
[HasOrderedElements]
|
||||
public enum SearchLanguage
|
||||
{
|
||||
@ -53,4 +57,61 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
[Order(13)]
|
||||
Other
|
||||
}
|
||||
|
||||
public class SearchLanguageEnumLocalisationMapper : EnumLocalisationMapper<SearchLanguage>
|
||||
{
|
||||
public override LocalisableString Map(SearchLanguage value)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case SearchLanguage.Any:
|
||||
return BeatmapsStrings.LanguageAny;
|
||||
|
||||
case SearchLanguage.Unspecified:
|
||||
return BeatmapsStrings.LanguageUnspecified;
|
||||
|
||||
case SearchLanguage.English:
|
||||
return BeatmapsStrings.LanguageEnglish;
|
||||
|
||||
case SearchLanguage.Japanese:
|
||||
return BeatmapsStrings.LanguageJapanese;
|
||||
|
||||
case SearchLanguage.Chinese:
|
||||
return BeatmapsStrings.LanguageChinese;
|
||||
|
||||
case SearchLanguage.Instrumental:
|
||||
return BeatmapsStrings.LanguageInstrumental;
|
||||
|
||||
case SearchLanguage.Korean:
|
||||
return BeatmapsStrings.LanguageKorean;
|
||||
|
||||
case SearchLanguage.French:
|
||||
return BeatmapsStrings.LanguageFrench;
|
||||
|
||||
case SearchLanguage.German:
|
||||
return BeatmapsStrings.LanguageGerman;
|
||||
|
||||
case SearchLanguage.Swedish:
|
||||
return BeatmapsStrings.LanguageSwedish;
|
||||
|
||||
case SearchLanguage.Spanish:
|
||||
return BeatmapsStrings.LanguageSpanish;
|
||||
|
||||
case SearchLanguage.Italian:
|
||||
return BeatmapsStrings.LanguageItalian;
|
||||
|
||||
case SearchLanguage.Russian:
|
||||
return BeatmapsStrings.LanguageRussian;
|
||||
|
||||
case SearchLanguage.Polish:
|
||||
return BeatmapsStrings.LanguagePolish;
|
||||
|
||||
case SearchLanguage.Other:
|
||||
return BeatmapsStrings.LanguageOther;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(value), value, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,38 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
|
||||
namespace osu.Game.Overlays.BeatmapListing
|
||||
{
|
||||
[LocalisableEnum(typeof(SearchPlayedEnumLocalisationMapper))]
|
||||
public enum SearchPlayed
|
||||
{
|
||||
Any,
|
||||
Played,
|
||||
Unplayed
|
||||
}
|
||||
|
||||
public class SearchPlayedEnumLocalisationMapper : EnumLocalisationMapper<SearchPlayed>
|
||||
{
|
||||
public override LocalisableString Map(SearchPlayed value)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case SearchPlayed.Any:
|
||||
return BeatmapsStrings.PlayedAny;
|
||||
|
||||
case SearchPlayed.Played:
|
||||
return BeatmapsStrings.PlayedPlayed;
|
||||
|
||||
case SearchPlayed.Unplayed:
|
||||
return BeatmapsStrings.PlayedUnplayed;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(value), value, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,13 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
|
||||
namespace osu.Game.Overlays.BeatmapListing
|
||||
{
|
||||
[LocalisableEnum(typeof(SortCriteriaLocalisationMapper))]
|
||||
public enum SortCriteria
|
||||
{
|
||||
Title,
|
||||
@ -14,4 +19,40 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
Favourites,
|
||||
Relevance
|
||||
}
|
||||
|
||||
public class SortCriteriaLocalisationMapper : EnumLocalisationMapper<SortCriteria>
|
||||
{
|
||||
public override LocalisableString Map(SortCriteria value)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case SortCriteria.Title:
|
||||
return BeatmapsStrings.ListingSearchSortingTitle;
|
||||
|
||||
case SortCriteria.Artist:
|
||||
return BeatmapsStrings.ListingSearchSortingArtist;
|
||||
|
||||
case SortCriteria.Difficulty:
|
||||
return BeatmapsStrings.ListingSearchSortingDifficulty;
|
||||
|
||||
case SortCriteria.Ranked:
|
||||
return BeatmapsStrings.ListingSearchSortingRanked;
|
||||
|
||||
case SortCriteria.Rating:
|
||||
return BeatmapsStrings.ListingSearchSortingRating;
|
||||
|
||||
case SortCriteria.Plays:
|
||||
return BeatmapsStrings.ListingSearchSortingPlays;
|
||||
|
||||
case SortCriteria.Favourites:
|
||||
return BeatmapsStrings.ListingSearchSortingFavourites;
|
||||
|
||||
case SortCriteria.Relevance:
|
||||
return BeatmapsStrings.ListingSearchSortingRelevance;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(value), value, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@ -22,8 +23,8 @@ namespace osu.Game.Overlays.BeatmapSet
|
||||
{
|
||||
private const float height = 50;
|
||||
|
||||
private readonly UpdateableAvatar avatar;
|
||||
private readonly FillFlowContainer fields;
|
||||
private UpdateableAvatar avatar;
|
||||
private FillFlowContainer fields;
|
||||
|
||||
private BeatmapSetInfo beatmapSet;
|
||||
|
||||
@ -35,11 +36,46 @@ namespace osu.Game.Overlays.BeatmapSet
|
||||
if (value == beatmapSet) return;
|
||||
|
||||
beatmapSet = value;
|
||||
|
||||
updateDisplay();
|
||||
Scheduler.AddOnce(updateDisplay);
|
||||
}
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Height = height;
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
CornerRadius = 4,
|
||||
Masking = true,
|
||||
Child = avatar = new UpdateableAvatar(showGuestOnNull: false)
|
||||
{
|
||||
Size = new Vector2(height),
|
||||
},
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Colour = Color4.Black.Opacity(0.25f),
|
||||
Type = EdgeEffectType.Shadow,
|
||||
Radius = 4,
|
||||
Offset = new Vector2(0f, 1f),
|
||||
},
|
||||
},
|
||||
fields = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Padding = new MarginPadding { Left = height + 5 },
|
||||
},
|
||||
};
|
||||
|
||||
Scheduler.AddOnce(updateDisplay);
|
||||
}
|
||||
|
||||
private void updateDisplay()
|
||||
{
|
||||
avatar.User = BeatmapSet?.Metadata.Author;
|
||||
@ -69,45 +105,6 @@ namespace osu.Game.Overlays.BeatmapSet
|
||||
}
|
||||
}
|
||||
|
||||
public AuthorInfo()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Height = height;
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
CornerRadius = 4,
|
||||
Masking = true,
|
||||
Child = avatar = new UpdateableAvatar
|
||||
{
|
||||
ShowGuestOnNull = false,
|
||||
Size = new Vector2(height),
|
||||
},
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Colour = Color4.Black.Opacity(0.25f),
|
||||
Type = EdgeEffectType.Shadow,
|
||||
Radius = 4,
|
||||
Offset = new Vector2(0f, 1f),
|
||||
},
|
||||
},
|
||||
fields = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Padding = new MarginPadding { Left = height + 5 },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private void load()
|
||||
{
|
||||
updateDisplay();
|
||||
}
|
||||
|
||||
private class Field : FillFlowContainer
|
||||
{
|
||||
public Field(string first, string second, FontUsage secondFont)
|
||||
|
@ -61,7 +61,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
|
||||
},
|
||||
}
|
||||
},
|
||||
avatar = new UpdateableAvatar
|
||||
avatar = new UpdateableAvatar(showGuestOnNull: false)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
@ -75,7 +75,6 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
|
||||
Offset = new Vector2(0, 2),
|
||||
Radius = 1,
|
||||
},
|
||||
ShowGuestOnNull = false,
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
|
@ -51,7 +51,7 @@ namespace osu.Game.Overlays.Chat.Tabs
|
||||
Child = new DelayedLoadWrapper(avatar = new ClickableAvatar(value.Users.First())
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
OpenOnClick = { Value = false },
|
||||
OpenOnClick = false,
|
||||
})
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
|
@ -2,6 +2,8 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Bindables;
|
||||
@ -66,6 +68,8 @@ namespace osu.Game.Overlays.Comments
|
||||
public readonly BindableBool Checked = new BindableBool();
|
||||
|
||||
private readonly SpriteIcon checkboxIcon;
|
||||
private Sample sampleChecked;
|
||||
private Sample sampleUnchecked;
|
||||
|
||||
public ShowDeletedButton()
|
||||
{
|
||||
@ -93,6 +97,13 @@ namespace osu.Game.Overlays.Comments
|
||||
});
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(AudioManager audio)
|
||||
{
|
||||
sampleChecked = audio.Samples.Get(@"UI/check-on");
|
||||
sampleUnchecked = audio.Samples.Get(@"UI/check-off");
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
Checked.BindValueChanged(isChecked => checkboxIcon.Icon = isChecked.NewValue ? FontAwesome.Solid.CheckSquare : FontAwesome.Regular.Square, true);
|
||||
@ -102,6 +113,12 @@ namespace osu.Game.Overlays.Comments
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
Checked.Value = !Checked.Value;
|
||||
|
||||
if (Checked.Value)
|
||||
sampleChecked?.Play();
|
||||
else
|
||||
sampleUnchecked?.Play();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
|
||||
namespace osu.Game.Overlays.Comments
|
||||
{
|
||||
@ -39,7 +38,6 @@ namespace osu.Game.Overlays.Comments
|
||||
Origin = Anchor.Centre,
|
||||
Margin = new MarginPadding { Horizontal = 10 }
|
||||
},
|
||||
new HoverClickSounds(),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
@ -13,6 +14,7 @@ using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
@ -27,7 +29,7 @@ namespace osu.Game.Overlays.KeyBinding
|
||||
public class KeyBindingRow : Container, IFilterable
|
||||
{
|
||||
private readonly object action;
|
||||
private readonly IEnumerable<Framework.Input.Bindings.KeyBinding> bindings;
|
||||
private readonly IEnumerable<RealmKeyBinding> bindings;
|
||||
|
||||
private const float transition_time = 150;
|
||||
|
||||
@ -62,7 +64,7 @@ namespace osu.Game.Overlays.KeyBinding
|
||||
|
||||
public IEnumerable<string> FilterTerms => bindings.Select(b => b.KeyCombination.ReadableString()).Prepend(text.Text.ToString());
|
||||
|
||||
public KeyBindingRow(object action, IEnumerable<Framework.Input.Bindings.KeyBinding> bindings)
|
||||
public KeyBindingRow(object action, List<RealmKeyBinding> bindings)
|
||||
{
|
||||
this.action = action;
|
||||
this.bindings = bindings;
|
||||
@ -72,7 +74,7 @@ namespace osu.Game.Overlays.KeyBinding
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private KeyBindingStore store { get; set; }
|
||||
private RealmContextFactory realmFactory { get; set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
@ -153,7 +155,8 @@ namespace osu.Game.Overlays.KeyBinding
|
||||
{
|
||||
var button = buttons[i++];
|
||||
button.UpdateKeyCombination(d);
|
||||
store.Update(button.KeyBinding);
|
||||
|
||||
updateStoreFromButton(button);
|
||||
}
|
||||
|
||||
isDefault.Value = true;
|
||||
@ -314,7 +317,7 @@ namespace osu.Game.Overlays.KeyBinding
|
||||
{
|
||||
if (bindTarget != null)
|
||||
{
|
||||
store.Update(bindTarget.KeyBinding);
|
||||
updateStoreFromButton(bindTarget);
|
||||
|
||||
updateIsDefaultValue();
|
||||
|
||||
@ -361,6 +364,17 @@ namespace osu.Game.Overlays.KeyBinding
|
||||
if (bindTarget != null) bindTarget.IsBinding = true;
|
||||
}
|
||||
|
||||
private void updateStoreFromButton(KeyButton button)
|
||||
{
|
||||
using (var usage = realmFactory.GetForWrite())
|
||||
{
|
||||
var binding = usage.Realm.Find<RealmKeyBinding>(((IHasGuidPrimaryKey)button.KeyBinding).ID);
|
||||
binding.KeyCombinationString = button.KeyBinding.KeyCombinationString;
|
||||
|
||||
usage.Commit();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateIsDefaultValue()
|
||||
{
|
||||
isDefault.Value = bindings.Select(b => b.KeyCombination).SequenceEqual(Defaults);
|
||||
@ -386,7 +400,7 @@ namespace osu.Game.Overlays.KeyBinding
|
||||
|
||||
public class KeyButton : Container
|
||||
{
|
||||
public readonly Framework.Input.Bindings.KeyBinding KeyBinding;
|
||||
public readonly RealmKeyBinding KeyBinding;
|
||||
|
||||
private readonly Box box;
|
||||
public readonly OsuSpriteText Text;
|
||||
@ -408,8 +422,11 @@ namespace osu.Game.Overlays.KeyBinding
|
||||
}
|
||||
}
|
||||
|
||||
public KeyButton(Framework.Input.Bindings.KeyBinding keyBinding)
|
||||
public KeyButton(RealmKeyBinding keyBinding)
|
||||
{
|
||||
if (keyBinding.IsManaged)
|
||||
throw new ArgumentException("Key binding should not be attached as we make temporary changes", nameof(keyBinding));
|
||||
|
||||
KeyBinding = keyBinding;
|
||||
|
||||
Margin = new MarginPadding(padding);
|
||||
@ -478,7 +495,7 @@ namespace osu.Game.Overlays.KeyBinding
|
||||
|
||||
public void UpdateKeyCombination(KeyCombination newCombination)
|
||||
{
|
||||
if ((KeyBinding as DatabasedKeyBinding)?.RulesetID != null && !KeyBindingStore.CheckValidForGameplay(newCombination))
|
||||
if (KeyBinding.RulesetID != null && !RealmKeyBindingStore.CheckValidForGameplay(newCombination))
|
||||
return;
|
||||
|
||||
KeyBinding.KeyCombination = newCombination;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user