1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-13 22:35:23 +08:00

Merge branch 'master' into scale-background-dim

This commit is contained in:
Dean Herbert 2023-05-02 14:24:54 +09:00
commit 7efaa299bd
164 changed files with 2084 additions and 690 deletions

View File

@ -105,7 +105,7 @@ When it comes to contributing to the project, the two main things you can do to
If you wish to help with localisation efforts, head over to [crowdin](https://crowdin.com/project/osu-web). If you wish to help with localisation efforts, head over to [crowdin](https://crowdin.com/project/osu-web).
For those interested, we love to reward quality contributions via [bounties](https://docs.google.com/spreadsheets/d/1jNXfj_S3Pb5PErA-czDdC9DUu4IgUbe1Lt8E7CYUJuE/view?&rm=minimal#gid=523803337), paid out via PayPal or osu!supporter tags. Don't hesitate to [request a bounty](https://docs.google.com/forms/d/e/1FAIpQLSet_8iFAgPMG526pBZ2Kic6HSh7XPM3fE8xPcnWNkMzINDdYg/viewform) for your work on this project. We love to reward quality contributions. If you have made a large contribution, or are a regular contributor, you are welcome to [submit an expense via opencollective](https://opencollective.com/ppy/expenses/new). If you have any questions, feel free to [reach out to peppy](mailto:pe@ppy.sh) before doing so.
## Licence ## Licence

View File

@ -11,7 +11,7 @@
<AndroidManifestMerger>manifestmerger.jar</AndroidManifestMerger> <AndroidManifestMerger>manifestmerger.jar</AndroidManifestMerger>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.327.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2023.418.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<AndroidManifestOverlay Include="$(MSBuildThisFileDirectory)osu.Android\Properties\AndroidManifestOverlay.xml" /> <AndroidManifestOverlay Include="$(MSBuildThisFileDirectory)osu.Android\Properties\AndroidManifestOverlay.xml" />

View File

@ -48,7 +48,6 @@ namespace osu.Game.Rulesets.Catch.Edit
private void load() private void load()
{ {
// todo: enable distance spacing once catch supports applying it to its existing distance snap grid implementation. // todo: enable distance spacing once catch supports applying it to its existing distance snap grid implementation.
RightSideToolboxContainer.Alpha = 0;
DistanceSpacingMultiplier.Disabled = true; DistanceSpacingMultiplier.Disabled = true;
LayerBelowRuleset.Add(new PlayfieldBorder LayerBelowRuleset.Add(new PlayfieldBorder

View File

@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Catch.Mods
{ {
public class CatchModDaycore : ModDaycore public class CatchModDaycore : ModDaycore
{ {
public override double ScoreMultiplier => 0.3;
} }
} }

View File

@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Catch.Mods
public DifficultyBindable CircleSize { get; } = new DifficultyBindable public DifficultyBindable CircleSize { get; } = new DifficultyBindable
{ {
Precision = 0.1f, Precision = 0.1f,
MinValue = 1, MinValue = 0,
MaxValue = 10, MaxValue = 10,
ExtendedMaxValue = 11, ExtendedMaxValue = 11,
ReadCurrentFromDifficulty = diff => diff.CircleSize, ReadCurrentFromDifficulty = diff => diff.CircleSize,
@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Catch.Mods
public DifficultyBindable ApproachRate { get; } = new DifficultyBindable public DifficultyBindable ApproachRate { get; } = new DifficultyBindable
{ {
Precision = 0.1f, Precision = 0.1f,
MinValue = 1, MinValue = 0,
MaxValue = 10, MaxValue = 10,
ExtendedMaxValue = 11, ExtendedMaxValue = 11,
ReadCurrentFromDifficulty = diff => diff.ApproachRate, ReadCurrentFromDifficulty = diff => diff.ApproachRate,

View File

@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Catch.Mods
{ {
public class CatchModDoubleTime : ModDoubleTime public class CatchModDoubleTime : ModDoubleTime
{ {
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;
} }
} }

View File

@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Catch.Mods
{ {
public class CatchModHalfTime : ModHalfTime public class CatchModHalfTime : ModHalfTime
{ {
public override double ScoreMultiplier => 0.3;
} }
} }

View File

@ -8,6 +8,5 @@ namespace osu.Game.Rulesets.Catch.Mods
{ {
public class CatchModNightcore : ModNightcore<CatchHitObject> public class CatchModNightcore : ModNightcore<CatchHitObject>
{ {
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;
} }
} }

View File

@ -136,6 +136,7 @@ namespace osu.Game.Rulesets.Catch.UI
Origin = Anchor.TopCentre; Origin = Anchor.TopCentre;
Size = new Vector2(BASE_SIZE); Size = new Vector2(BASE_SIZE);
if (difficulty != null) if (difficulty != null)
Scale = calculateScale(difficulty); Scale = calculateScale(difficulty);
@ -333,8 +334,11 @@ namespace osu.Game.Rulesets.Catch.UI
base.Update(); base.Update();
var scaleFromDirection = new Vector2((int)VisualDirection, 1); var scaleFromDirection = new Vector2((int)VisualDirection, 1);
body.Scale = scaleFromDirection; body.Scale = scaleFromDirection;
caughtObjectContainer.Scale = hitExplosionContainer.Scale = flipCatcherPlate ? scaleFromDirection : Vector2.One; // Inverse of catcher scale is applied here, as catcher gets scaled by circle size and so do the incoming fruit.
caughtObjectContainer.Scale = (1 / Scale.X) * (flipCatcherPlate ? scaleFromDirection : Vector2.One);
hitExplosionContainer.Scale = flipCatcherPlate ? scaleFromDirection : Vector2.One;
// Correct overshooting. // Correct overshooting.
if ((hyperDashDirection > 0 && hyperDashTargetPosition < X) || if ((hyperDashDirection > 0 && hyperDashTargetPosition < X) ||
@ -414,10 +418,13 @@ namespace osu.Game.Rulesets.Catch.UI
private void clearPlate(DroppedObjectAnimation animation) private void clearPlate(DroppedObjectAnimation animation)
{ {
var droppedObjects = caughtObjectContainer.Children.Select(getDroppedObject).ToArray(); var caughtObjects = caughtObjectContainer.Children.ToArray();
caughtObjectContainer.Clear(false); caughtObjectContainer.Clear(false);
// Use the already returned PoolableDrawables for new objects
var droppedObjects = caughtObjects.Select(getDroppedObject).ToArray();
droppedObjectTarget.AddRange(droppedObjects); droppedObjectTarget.AddRange(droppedObjects);
foreach (var droppedObject in droppedObjects) foreach (var droppedObject in droppedObjects)
@ -426,10 +433,10 @@ namespace osu.Game.Rulesets.Catch.UI
private void removeFromPlate(CaughtObject caughtObject, DroppedObjectAnimation animation) private void removeFromPlate(CaughtObject caughtObject, DroppedObjectAnimation animation)
{ {
var droppedObject = getDroppedObject(caughtObject);
caughtObjectContainer.Remove(caughtObject, false); caughtObjectContainer.Remove(caughtObject, false);
var droppedObject = getDroppedObject(caughtObject);
droppedObjectTarget.Add(droppedObject); droppedObjectTarget.Add(droppedObject);
applyDropAnimation(droppedObject, animation); applyDropAnimation(droppedObject, animation);
@ -452,6 +459,8 @@ namespace osu.Game.Rulesets.Catch.UI
break; break;
} }
// Define lifetime start for dropped objects to be disposed correctly when rewinding replay
d.LifetimeStart = Clock.CurrentTime;
d.Expire(); d.Expire();
} }

View File

@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Mania.Mods
{ {
public class ManiaModDaycore : ModDaycore public class ManiaModDaycore : ModDaycore
{ {
public override double ScoreMultiplier => 0.5;
} }
} }

View File

@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Mania.Mods
{ {
public class ManiaModDoubleTime : ModDoubleTime public class ManiaModDoubleTime : ModDoubleTime
{ {
public override double ScoreMultiplier => 1;
} }
} }

View File

@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Mania.Mods
{ {
public class ManiaModHalfTime : ModHalfTime public class ManiaModHalfTime : ModHalfTime
{ {
public override double ScoreMultiplier => 0.5;
} }
} }

View File

@ -8,6 +8,5 @@ namespace osu.Game.Rulesets.Mania.Mods
{ {
public class ManiaModNightcore : ModNightcore<ManiaHitObject> public class ManiaModNightcore : ModNightcore<ManiaHitObject>
{ {
public override double ScoreMultiplier => 1;
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -8,6 +8,7 @@ using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
@ -18,6 +19,7 @@ using osu.Framework.Testing.Input;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets.Osu.Configuration;
using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
@ -40,6 +42,8 @@ namespace osu.Game.Rulesets.Osu.Tests
private Drawable background; private Drawable background;
private readonly Bindable<bool> ripples = new Bindable<bool>();
public TestSceneGameplayCursor() public TestSceneGameplayCursor()
{ {
var ruleset = new OsuRuleset(); var ruleset = new OsuRuleset();
@ -57,6 +61,8 @@ namespace osu.Game.Rulesets.Osu.Tests
}); });
}); });
AddToggleStep("ripples", v => ripples.Value = v);
AddSliderStep("circle size", 0f, 10f, 0f, val => AddSliderStep("circle size", 0f, 10f, 0f, val =>
{ {
config.SetValue(OsuSetting.AutoCursorSize, true); config.SetValue(OsuSetting.AutoCursorSize, true);
@ -67,6 +73,13 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep("test cursor container", () => loadContent(false)); AddStep("test cursor container", () => loadContent(false));
} }
[BackgroundDependencyLoader]
private void load()
{
var rulesetConfig = (OsuRulesetConfigManager)RulesetConfigs.GetConfigFor(Ruleset.Value.CreateInstance()).AsNonNull();
rulesetConfig.BindWith(OsuRulesetSetting.ShowCursorRipples, ripples);
}
[TestCase(1, 1)] [TestCase(1, 1)]
[TestCase(5, 1)] [TestCase(5, 1)]
[TestCase(10, 1)] [TestCase(10, 1)]

View File

@ -21,7 +21,7 @@ using osu.Game.Configuration;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -34,9 +34,9 @@ namespace osu.Game.Rulesets.Osu.Tests
[Resolved] [Resolved]
private OsuConfigManager config { get; set; } = null!; private OsuConfigManager config { get; set; } = null!;
private TestActionKeyCounter leftKeyCounter = null!; private DefaultKeyCounter leftKeyCounter = null!;
private TestActionKeyCounter rightKeyCounter = null!; private DefaultKeyCounter rightKeyCounter = null!;
private OsuInputManager osuInputManager = null!; private OsuInputManager osuInputManager = null!;
@ -59,14 +59,14 @@ namespace osu.Game.Rulesets.Osu.Tests
Origin = Anchor.Centre, Origin = Anchor.Centre,
Children = new Drawable[] Children = new Drawable[]
{ {
leftKeyCounter = new TestActionKeyCounter(OsuAction.LeftButton) leftKeyCounter = new DefaultKeyCounter(new TestActionKeyCounterTrigger(OsuAction.LeftButton))
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.CentreRight, Origin = Anchor.CentreRight,
Depth = float.MinValue, Depth = float.MinValue,
X = -100, X = -100,
}, },
rightKeyCounter = new TestActionKeyCounter(OsuAction.RightButton) rightKeyCounter = new DefaultKeyCounter(new TestActionKeyCounterTrigger(OsuAction.RightButton))
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
@ -598,8 +598,8 @@ namespace osu.Game.Rulesets.Osu.Tests
private void assertKeyCounter(int left, int right) private void assertKeyCounter(int left, int right)
{ {
AddAssert($"The left key was pressed {left} times", () => leftKeyCounter.CountPresses, () => Is.EqualTo(left)); AddAssert($"The left key was pressed {left} times", () => leftKeyCounter.CountPresses.Value, () => Is.EqualTo(left));
AddAssert($"The right key was pressed {right} times", () => rightKeyCounter.CountPresses, () => Is.EqualTo(right)); AddAssert($"The right key was pressed {right} times", () => rightKeyCounter.CountPresses.Value, () => Is.EqualTo(right));
} }
private void releaseAllTouches() private void releaseAllTouches()
@ -615,11 +615,11 @@ namespace osu.Game.Rulesets.Osu.Tests
private void checkNotPressed(OsuAction action) => AddAssert($"Not pressing {action}", () => !osuInputManager.PressedActions.Contains(action)); private void checkNotPressed(OsuAction action) => AddAssert($"Not pressing {action}", () => !osuInputManager.PressedActions.Contains(action));
private void checkPressed(OsuAction action) => AddAssert($"Is pressing {action}", () => osuInputManager.PressedActions.Contains(action)); private void checkPressed(OsuAction action) => AddAssert($"Is pressing {action}", () => osuInputManager.PressedActions.Contains(action));
public partial class TestActionKeyCounter : KeyCounter, IKeyBindingHandler<OsuAction> public partial class TestActionKeyCounterTrigger : InputTrigger, IKeyBindingHandler<OsuAction>
{ {
public OsuAction Action { get; } public OsuAction Action { get; }
public TestActionKeyCounter(OsuAction action) public TestActionKeyCounterTrigger(OsuAction action)
: base(action.ToString()) : base(action.ToString())
{ {
Action = action; Action = action;
@ -629,8 +629,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
if (e.Action == Action) if (e.Action == Action)
{ {
IsLit = true; Activate();
Increment();
} }
return false; return false;
@ -638,7 +637,8 @@ namespace osu.Game.Rulesets.Osu.Tests
public void OnReleased(KeyBindingReleaseEvent<OsuAction> e) public void OnReleased(KeyBindingReleaseEvent<OsuAction> e)
{ {
if (e.Action == Action) IsLit = false; if (e.Action == Action)
Deactivate();
} }
} }

View File

@ -22,6 +22,7 @@ namespace osu.Game.Rulesets.Osu.Configuration
SetDefault(OsuRulesetSetting.SnakingInSliders, true); SetDefault(OsuRulesetSetting.SnakingInSliders, true);
SetDefault(OsuRulesetSetting.SnakingOutSliders, true); SetDefault(OsuRulesetSetting.SnakingOutSliders, true);
SetDefault(OsuRulesetSetting.ShowCursorTrail, true); SetDefault(OsuRulesetSetting.ShowCursorTrail, true);
SetDefault(OsuRulesetSetting.ShowCursorRipples, false);
SetDefault(OsuRulesetSetting.PlayfieldBorderStyle, PlayfieldBorderStyle.None); SetDefault(OsuRulesetSetting.PlayfieldBorderStyle, PlayfieldBorderStyle.None);
} }
} }
@ -31,6 +32,7 @@ namespace osu.Game.Rulesets.Osu.Configuration
SnakingInSliders, SnakingInSliders,
SnakingOutSliders, SnakingOutSliders,
ShowCursorTrail, ShowCursorTrail,
ShowCursorRipples,
PlayfieldBorderStyle, PlayfieldBorderStyle,
} }
} }

View File

@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
protected override bool AlwaysShowWhenSelected => true; protected override bool AlwaysShowWhenSelected => true;
protected override bool ShouldBeAlive => base.ShouldBeAlive protected override bool ShouldBeAlive => base.ShouldBeAlive
|| (ShowHitMarkers.Value && editorClock.CurrentTime >= Item.StartTime && editorClock.CurrentTime - Item.GetEndTime() < HitCircleOverlapMarker.FADE_OUT_EXTENSION); || (DrawableObject is not DrawableSpinner && ShowHitMarkers.Value && editorClock.CurrentTime >= Item.StartTime && editorClock.CurrentTime - Item.GetEndTime() < HitCircleOverlapMarker.FADE_OUT_EXTENSION);
protected OsuSelectionBlueprint(T hitObject) protected OsuSelectionBlueprint(T hitObject)
: base(hitObject) : base(hitObject)

View File

@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModDaycore : ModDaycore public class OsuModDaycore : ModDaycore
{ {
public override double ScoreMultiplier => 0.3;
} }
} }

View File

@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModDoubleTime : ModDoubleTime public class OsuModDoubleTime : ModDoubleTime
{ {
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
} }
} }

View File

@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModHalfTime : ModHalfTime public class OsuModHalfTime : ModHalfTime
{ {
public override double ScoreMultiplier => 0.3;
} }
} }

View File

@ -8,6 +8,5 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModNightcore : ModNightcore<OsuHitObject> public class OsuModNightcore : ModNightcore<OsuHitObject>
{ {
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
} }
} }

View File

@ -10,6 +10,7 @@ namespace osu.Game.Rulesets.Osu
Cursor, Cursor,
CursorTrail, CursorTrail,
CursorParticles, CursorParticles,
CursorRipple,
SliderScorePoint, SliderScorePoint,
ReverseArrow, ReverseArrow,
HitCircleText, HitCircleText,

View File

@ -100,6 +100,28 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
return null; return null;
case OsuSkinComponents.CursorRipple:
if (GetTexture("cursor-ripple") != null)
{
var ripple = this.GetAnimation("cursor-ripple", false, false);
// In stable this element was scaled down to 50% and opacity 20%, but this makes the elements WAY too big and inflexible.
// If anyone complains about these not being applied, this can be uncommented.
//
// But if no one complains I'd rather fix this in lazer. Wiki documentation doesn't mention size,
// so we might be okay.
//
// if (ripple != null)
// {
// ripple.Scale = new Vector2(0.5f);
// ripple.Alpha = 0.2f;
// }
return ripple;
}
return null;
case OsuSkinComponents.CursorParticles: case OsuSkinComponents.CursorParticles:
if (GetTexture("star2") != null) if (GetTexture("star2") != null)
return new LegacyCursorParticles(); return new LegacyCursorParticles();

View File

@ -0,0 +1,105 @@
// 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.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Osu.Configuration;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Osu.UI.Cursor
{
public partial class CursorRippleVisualiser : CompositeDrawable, IKeyBindingHandler<OsuAction>
{
private readonly Bindable<bool> showRipples = new Bindable<bool>(true);
private readonly DrawablePool<CursorRipple> ripplePool = new DrawablePool<CursorRipple>(20);
public CursorRippleVisualiser()
{
RelativeSizeAxes = Axes.Both;
}
public Vector2 CursorScale { get; set; } = Vector2.One;
[BackgroundDependencyLoader(true)]
private void load(OsuRulesetConfigManager? rulesetConfig)
{
rulesetConfig?.BindWith(OsuRulesetSetting.ShowCursorRipples, showRipples);
}
public bool OnPressed(KeyBindingPressEvent<OsuAction> e)
{
if (showRipples.Value)
{
AddInternal(ripplePool.Get(r =>
{
r.Position = e.MousePosition;
r.Scale = CursorScale;
}));
}
return false;
}
public void OnReleased(KeyBindingReleaseEvent<OsuAction> e)
{
}
private partial class CursorRipple : PoolableDrawable
{
private Drawable ripple = null!;
[BackgroundDependencyLoader]
private void load()
{
AutoSizeAxes = Axes.Both;
Origin = Anchor.Centre;
InternalChild = ripple = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.CursorRipple), _ => new DefaultCursorRipple())
{
Blending = BlendingParameters.Additive,
};
}
protected override void PrepareForUse()
{
base.PrepareForUse();
ClearTransforms(true);
ripple.ScaleTo(0.1f)
.ScaleTo(1, 700, Easing.Out);
this
.FadeOutFromOne(700)
.Expire(true);
}
}
public partial class DefaultCursorRipple : CompositeDrawable
{
[BackgroundDependencyLoader]
private void load()
{
AutoSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
new RingPiece(3)
{
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2),
Alpha = 0.1f,
}
};
}
}
}
}

View File

@ -40,6 +40,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private Bindable<float> userCursorScale; private Bindable<float> userCursorScale;
private Bindable<bool> autoCursorScale; private Bindable<bool> autoCursorScale;
private readonly CursorRippleVisualiser rippleVisualiser;
public OsuCursorContainer() public OsuCursorContainer()
{ {
InternalChild = fadeContainer = new Container InternalChild = fadeContainer = new Container
@ -48,6 +50,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
Children = new[] Children = new[]
{ {
cursorTrail = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.CursorTrail), _ => new DefaultCursorTrail(), confineMode: ConfineMode.NoScaling), cursorTrail = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.CursorTrail), _ => new DefaultCursorTrail(), confineMode: ConfineMode.NoScaling),
rippleVisualiser = new CursorRippleVisualiser(),
new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.CursorParticles), confineMode: ConfineMode.NoScaling), new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.CursorParticles), confineMode: ConfineMode.NoScaling),
} }
}; };
@ -82,6 +85,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
var newScale = new Vector2(e.NewValue); var newScale = new Vector2(e.NewValue);
ActiveCursor.Scale = newScale; ActiveCursor.Scale = newScale;
rippleVisualiser.CursorScale = newScale;
cursorTrail.Scale = newScale; cursorTrail.Scale = newScale;
}, true); }, true);

View File

@ -43,6 +43,11 @@ namespace osu.Game.Rulesets.Osu.UI
LabelText = RulesetSettingsStrings.CursorTrail, LabelText = RulesetSettingsStrings.CursorTrail,
Current = config.GetBindable<bool>(OsuRulesetSetting.ShowCursorTrail) Current = config.GetBindable<bool>(OsuRulesetSetting.ShowCursorTrail)
}, },
new SettingsCheckbox
{
LabelText = RulesetSettingsStrings.CursorRipples,
Current = config.GetBindable<bool>(OsuRulesetSetting.ShowCursorRipples)
},
new SettingsEnumDropdown<PlayfieldBorderStyle> new SettingsEnumDropdown<PlayfieldBorderStyle>
{ {
LabelText = RulesetSettingsStrings.PlayfieldBorderStyle, LabelText = RulesetSettingsStrings.PlayfieldBorderStyle,

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects;
@ -35,20 +33,11 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
protected override bool OnMouseDown(MouseDownEvent e) protected override bool OnMouseDown(MouseDownEvent e)
{ {
switch (e.Button) if (e.Button != MouseButton.Left)
{ return false;
case MouseButton.Left:
HitObject.Type = HitType.Centre;
EndPlacement(true);
return true;
case MouseButton.Right: EndPlacement(true);
HitObject.Type = HitType.Rim; return true;
EndPlacement(true);
return true;
}
return false;
} }
public override void UpdateTimeAndPosition(SnapResult result) public override void UpdateTimeAndPosition(SnapResult result)

View File

@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
public void ApplyToDrawableRuleset(DrawableRuleset<TaikoHitObject> drawableRuleset) public void ApplyToDrawableRuleset(DrawableRuleset<TaikoHitObject> drawableRuleset)
{ {
var drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset; var drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset;
drawableTaikoRuleset.LockPlayfieldMaxAspect.Value = false; drawableTaikoRuleset.LockPlayfieldAspectRange.Value = false;
var playfield = (TaikoPlayfield)drawableRuleset.Playfield; var playfield = (TaikoPlayfield)drawableRuleset.Playfield;
playfield.ClassicHitTargetPosition.Value = true; playfield.ClassicHitTargetPosition.Value = true;

View File

@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Taiko.Mods
{ {
public class TaikoModDaycore : ModDaycore public class TaikoModDaycore : ModDaycore
{ {
public override double ScoreMultiplier => 0.3;
} }
} }

View File

@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Taiko.Mods
{ {
public class TaikoModDoubleTime : ModDoubleTime public class TaikoModDoubleTime : ModDoubleTime
{ {
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
} }
} }

View File

@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Taiko.Mods
{ {
public class TaikoModHalfTime : ModHalfTime public class TaikoModHalfTime : ModHalfTime
{ {
public override double ScoreMultiplier => 0.3;
} }
} }

View File

@ -8,6 +8,5 @@ namespace osu.Game.Rulesets.Taiko.Mods
{ {
public class TaikoModNightcore : ModNightcore<TaikoHitObject> public class TaikoModNightcore : ModNightcore<TaikoHitObject>
{ {
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
} }
} }

View File

@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Taiko.UI
{ {
public new BindableDouble TimeRange => base.TimeRange; public new BindableDouble TimeRange => base.TimeRange;
public readonly BindableBool LockPlayfieldMaxAspect = new BindableBool(true); public readonly BindableBool LockPlayfieldAspectRange = new BindableBool(true);
public new TaikoInputManager KeyBindingInputManager => (TaikoInputManager)base.KeyBindingInputManager; public new TaikoInputManager KeyBindingInputManager => (TaikoInputManager)base.KeyBindingInputManager;
@ -69,7 +69,9 @@ namespace osu.Game.Rulesets.Taiko.UI
const float scroll_rate = 10; const float scroll_rate = 10;
// Since the time range will depend on a positional value, it is referenced to the x480 pixel space. // Since the time range will depend on a positional value, it is referenced to the x480 pixel space.
float ratio = DrawHeight / 480; // Width is used because it defines how many notes fit on the playfield.
// We clamp the ratio to the maximum aspect ratio to keep scroll speed consistent on widths lower than the default.
float ratio = Math.Max(DrawSize.X / 768f, TaikoPlayfieldAdjustmentContainer.MAXIMUM_ASPECT);
TimeRange.Value = (Playfield.HitObjectContainer.DrawWidth / ratio) * scroll_rate; TimeRange.Value = (Playfield.HitObjectContainer.DrawWidth / ratio) * scroll_rate;
} }
@ -92,7 +94,7 @@ namespace osu.Game.Rulesets.Taiko.UI
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new TaikoPlayfieldAdjustmentContainer public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new TaikoPlayfieldAdjustmentContainer
{ {
LockPlayfieldMaxAspect = { BindTarget = LockPlayfieldMaxAspect } LockPlayfieldAspectRange = { BindTarget = LockPlayfieldAspectRange }
}; };
protected override PassThroughInputManager CreateInputManager() => new TaikoInputManager(Ruleset.RulesetInfo); protected override PassThroughInputManager CreateInputManager() => new TaikoInputManager(Ruleset.RulesetInfo);

View File

@ -11,9 +11,11 @@ namespace osu.Game.Rulesets.Taiko.UI
public partial class TaikoPlayfieldAdjustmentContainer : PlayfieldAdjustmentContainer public partial class TaikoPlayfieldAdjustmentContainer : PlayfieldAdjustmentContainer
{ {
private const float default_relative_height = TaikoPlayfield.DEFAULT_HEIGHT / 768; private const float default_relative_height = TaikoPlayfield.DEFAULT_HEIGHT / 768;
private const float default_aspect = 16f / 9f;
public readonly IBindable<bool> LockPlayfieldMaxAspect = new BindableBool(true); public const float MAXIMUM_ASPECT = 16f / 9f;
public const float MINIMUM_ASPECT = 5f / 4f;
public readonly IBindable<bool> LockPlayfieldAspectRange = new BindableBool(true);
protected override void Update() protected override void Update()
{ {
@ -26,12 +28,22 @@ namespace osu.Game.Rulesets.Taiko.UI
// //
// As a middle-ground, the aspect ratio can still be adjusted in the downwards direction but has a maximum limit. // As a middle-ground, the aspect ratio can still be adjusted in the downwards direction but has a maximum limit.
// This is still a bit weird, because readability changes with window size, but it is what it is. // This is still a bit weird, because readability changes with window size, but it is what it is.
if (LockPlayfieldMaxAspect.Value && Parent.ChildSize.X / Parent.ChildSize.Y > default_aspect) if (LockPlayfieldAspectRange.Value)
height *= Math.Clamp(Parent.ChildSize.X / Parent.ChildSize.Y, 0.4f, 4) / default_aspect; {
float currentAspect = Parent.ChildSize.X / Parent.ChildSize.Y;
if (currentAspect > MAXIMUM_ASPECT)
height *= currentAspect / MAXIMUM_ASPECT;
else if (currentAspect < MINIMUM_ASPECT)
height *= currentAspect / MINIMUM_ASPECT;
}
// Limit the maximum relative height of the playfield to one-third of available area to avoid it masking out on extreme resolutions.
height = Math.Min(height, 1f / 3f);
Height = height; Height = height;
// Position the taiko playfield exactly one playfield from the top of the screen. // Position the taiko playfield exactly one playfield from the top of the screen, if there is enough space for it.
// Note that the relative height cannot exceed one-third - if that limit is hit, the playfield will be exactly centered.
RelativePositionAxes = Axes.Y; RelativePositionAxes = Axes.Y;
Y = height; Y = height;
} }

View File

@ -1,9 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
@ -161,6 +160,51 @@ namespace osu.Game.Tests.Beatmaps.Formats
} }
} }
[Test]
public void TestDecodeVideoWithLowercaseExtension()
{
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
using (var resStream = TestResources.OpenResource("video-with-lowercase-extension.osb"))
using (var stream = new LineBufferedReader(resStream))
{
var beatmap = decoder.Decode(stream);
var metadata = beatmap.Metadata;
Assert.AreEqual("BG.jpg", metadata.BackgroundFile);
}
}
[Test]
public void TestDecodeVideoWithUppercaseExtension()
{
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
using (var resStream = TestResources.OpenResource("video-with-uppercase-extension.osb"))
using (var stream = new LineBufferedReader(resStream))
{
var beatmap = decoder.Decode(stream);
var metadata = beatmap.Metadata;
Assert.AreEqual("BG.jpg", metadata.BackgroundFile);
}
}
[Test]
public void TestDecodeImageSpecifiedAsVideo()
{
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
using (var resStream = TestResources.OpenResource("image-specified-as-video.osb"))
using (var stream = new LineBufferedReader(resStream))
{
var beatmap = decoder.Decode(stream);
var metadata = beatmap.Metadata;
Assert.AreEqual("BG.jpg", metadata.BackgroundFile);
}
}
[Test] [Test]
public void TestDecodeBeatmapTimingPoints() public void TestDecodeBeatmapTimingPoints()
{ {
@ -320,6 +364,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
{ {
var comboColors = decoder.Decode(stream).ComboColours; var comboColors = decoder.Decode(stream).ComboColours;
Debug.Assert(comboColors != null);
Color4[] expectedColors = Color4[] expectedColors =
{ {
new Color4(142, 199, 255, 255), new Color4(142, 199, 255, 255),
@ -330,7 +376,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
new Color4(255, 177, 140, 255), new Color4(255, 177, 140, 255),
new Color4(100, 100, 100, 255), // alpha is specified as 100, but should be ignored. new Color4(100, 100, 100, 255), // alpha is specified as 100, but should be ignored.
}; };
Assert.AreEqual(expectedColors.Length, comboColors?.Count); Assert.AreEqual(expectedColors.Length, comboColors.Count);
for (int i = 0; i < expectedColors.Length; i++) for (int i = 0; i < expectedColors.Length; i++)
Assert.AreEqual(expectedColors[i], comboColors[i]); Assert.AreEqual(expectedColors[i], comboColors[i]);
} }
@ -415,14 +461,14 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.IsNotNull(positionData); Assert.IsNotNull(positionData);
Assert.IsNotNull(curveData); Assert.IsNotNull(curveData);
Assert.AreEqual(new Vector2(192, 168), positionData.Position); Assert.AreEqual(new Vector2(192, 168), positionData!.Position);
Assert.AreEqual(956, hitObjects[0].StartTime); Assert.AreEqual(956, hitObjects[0].StartTime);
Assert.IsTrue(hitObjects[0].Samples.Any(s => s.Name == HitSampleInfo.HIT_NORMAL)); Assert.IsTrue(hitObjects[0].Samples.Any(s => s.Name == HitSampleInfo.HIT_NORMAL));
positionData = hitObjects[1] as IHasPosition; positionData = hitObjects[1] as IHasPosition;
Assert.IsNotNull(positionData); Assert.IsNotNull(positionData);
Assert.AreEqual(new Vector2(304, 56), positionData.Position); Assert.AreEqual(new Vector2(304, 56), positionData!.Position);
Assert.AreEqual(1285, hitObjects[1].StartTime); Assert.AreEqual(1285, hitObjects[1].StartTime);
Assert.IsTrue(hitObjects[1].Samples.Any(s => s.Name == HitSampleInfo.HIT_CLAP)); Assert.IsTrue(hitObjects[1].Samples.Any(s => s.Name == HitSampleInfo.HIT_CLAP));
} }
@ -578,8 +624,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
[Test] [Test]
public void TestFallbackDecoderForCorruptedHeader() public void TestFallbackDecoderForCorruptedHeader()
{ {
Decoder<Beatmap> decoder = null; Decoder<Beatmap> decoder = null!;
Beatmap beatmap = null; Beatmap beatmap = null!;
using (var resStream = TestResources.OpenResource("corrupted-header.osu")) using (var resStream = TestResources.OpenResource("corrupted-header.osu"))
using (var stream = new LineBufferedReader(resStream)) using (var stream = new LineBufferedReader(resStream))
@ -596,8 +642,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
[Test] [Test]
public void TestFallbackDecoderForMissingHeader() public void TestFallbackDecoderForMissingHeader()
{ {
Decoder<Beatmap> decoder = null; Decoder<Beatmap> decoder = null!;
Beatmap beatmap = null; Beatmap beatmap = null!;
using (var resStream = TestResources.OpenResource("missing-header.osu")) using (var resStream = TestResources.OpenResource("missing-header.osu"))
using (var stream = new LineBufferedReader(resStream)) using (var stream = new LineBufferedReader(resStream))
@ -614,8 +660,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
[Test] [Test]
public void TestDecodeFileWithEmptyLinesAtStart() public void TestDecodeFileWithEmptyLinesAtStart()
{ {
Decoder<Beatmap> decoder = null; Decoder<Beatmap> decoder = null!;
Beatmap beatmap = null; Beatmap beatmap = null!;
using (var resStream = TestResources.OpenResource("empty-lines-at-start.osu")) using (var resStream = TestResources.OpenResource("empty-lines-at-start.osu"))
using (var stream = new LineBufferedReader(resStream)) using (var stream = new LineBufferedReader(resStream))
@ -632,8 +678,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
[Test] [Test]
public void TestDecodeFileWithEmptyLinesAndNoHeader() public void TestDecodeFileWithEmptyLinesAndNoHeader()
{ {
Decoder<Beatmap> decoder = null; Decoder<Beatmap> decoder = null!;
Beatmap beatmap = null; Beatmap beatmap = null!;
using (var resStream = TestResources.OpenResource("empty-line-instead-of-header.osu")) using (var resStream = TestResources.OpenResource("empty-line-instead-of-header.osu"))
using (var stream = new LineBufferedReader(resStream)) using (var stream = new LineBufferedReader(resStream))
@ -650,8 +696,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
[Test] [Test]
public void TestDecodeFileWithContentImmediatelyAfterHeader() public void TestDecodeFileWithContentImmediatelyAfterHeader()
{ {
Decoder<Beatmap> decoder = null; Decoder<Beatmap> decoder = null!;
Beatmap beatmap = null; Beatmap beatmap = null!;
using (var resStream = TestResources.OpenResource("no-empty-line-after-header.osu")) using (var resStream = TestResources.OpenResource("no-empty-line-after-header.osu"))
using (var stream = new LineBufferedReader(resStream)) using (var stream = new LineBufferedReader(resStream))
@ -678,7 +724,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
[Test] [Test]
public void TestAllowFallbackDecoderOverwrite() public void TestAllowFallbackDecoderOverwrite()
{ {
Decoder<Beatmap> decoder = null; Decoder<Beatmap> decoder = null!;
using (var resStream = TestResources.OpenResource("corrupted-header.osu")) using (var resStream = TestResources.OpenResource("corrupted-header.osu"))
using (var stream = new LineBufferedReader(resStream)) using (var stream = new LineBufferedReader(resStream))

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osuTK; using osuTK;
@ -30,35 +28,35 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.IsTrue(storyboard.HasDrawable); Assert.IsTrue(storyboard.HasDrawable);
Assert.AreEqual(6, storyboard.Layers.Count()); Assert.AreEqual(6, storyboard.Layers.Count());
StoryboardLayer background = storyboard.Layers.FirstOrDefault(l => l.Depth == 3); StoryboardLayer background = storyboard.Layers.Single(l => l.Depth == 3);
Assert.IsNotNull(background); Assert.IsNotNull(background);
Assert.AreEqual(16, background.Elements.Count); Assert.AreEqual(16, background.Elements.Count);
Assert.IsTrue(background.VisibleWhenFailing); Assert.IsTrue(background.VisibleWhenFailing);
Assert.IsTrue(background.VisibleWhenPassing); Assert.IsTrue(background.VisibleWhenPassing);
Assert.AreEqual("Background", background.Name); Assert.AreEqual("Background", background.Name);
StoryboardLayer fail = storyboard.Layers.FirstOrDefault(l => l.Depth == 2); StoryboardLayer fail = storyboard.Layers.Single(l => l.Depth == 2);
Assert.IsNotNull(fail); Assert.IsNotNull(fail);
Assert.AreEqual(0, fail.Elements.Count); Assert.AreEqual(0, fail.Elements.Count);
Assert.IsTrue(fail.VisibleWhenFailing); Assert.IsTrue(fail.VisibleWhenFailing);
Assert.IsFalse(fail.VisibleWhenPassing); Assert.IsFalse(fail.VisibleWhenPassing);
Assert.AreEqual("Fail", fail.Name); Assert.AreEqual("Fail", fail.Name);
StoryboardLayer pass = storyboard.Layers.FirstOrDefault(l => l.Depth == 1); StoryboardLayer pass = storyboard.Layers.Single(l => l.Depth == 1);
Assert.IsNotNull(pass); Assert.IsNotNull(pass);
Assert.AreEqual(0, pass.Elements.Count); Assert.AreEqual(0, pass.Elements.Count);
Assert.IsFalse(pass.VisibleWhenFailing); Assert.IsFalse(pass.VisibleWhenFailing);
Assert.IsTrue(pass.VisibleWhenPassing); Assert.IsTrue(pass.VisibleWhenPassing);
Assert.AreEqual("Pass", pass.Name); Assert.AreEqual("Pass", pass.Name);
StoryboardLayer foreground = storyboard.Layers.FirstOrDefault(l => l.Depth == 0); StoryboardLayer foreground = storyboard.Layers.Single(l => l.Depth == 0);
Assert.IsNotNull(foreground); Assert.IsNotNull(foreground);
Assert.AreEqual(151, foreground.Elements.Count); Assert.AreEqual(151, foreground.Elements.Count);
Assert.IsTrue(foreground.VisibleWhenFailing); Assert.IsTrue(foreground.VisibleWhenFailing);
Assert.IsTrue(foreground.VisibleWhenPassing); Assert.IsTrue(foreground.VisibleWhenPassing);
Assert.AreEqual("Foreground", foreground.Name); Assert.AreEqual("Foreground", foreground.Name);
StoryboardLayer overlay = storyboard.Layers.FirstOrDefault(l => l.Depth == int.MinValue); StoryboardLayer overlay = storyboard.Layers.Single(l => l.Depth == int.MinValue);
Assert.IsNotNull(overlay); Assert.IsNotNull(overlay);
Assert.IsEmpty(overlay.Elements); Assert.IsEmpty(overlay.Elements);
Assert.IsTrue(overlay.VisibleWhenFailing); Assert.IsTrue(overlay.VisibleWhenFailing);
@ -76,7 +74,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var sprite = background.Elements.ElementAt(0) as StoryboardSprite; var sprite = background.Elements.ElementAt(0) as StoryboardSprite;
Assert.NotNull(sprite); Assert.NotNull(sprite);
Assert.IsTrue(sprite.HasCommands); Assert.IsTrue(sprite!.HasCommands);
Assert.AreEqual(new Vector2(320, 240), sprite.InitialPosition); Assert.AreEqual(new Vector2(320, 240), sprite.InitialPosition);
Assert.IsTrue(sprite.IsDrawable); Assert.IsTrue(sprite.IsDrawable);
Assert.AreEqual(Anchor.Centre, sprite.Origin); Assert.AreEqual(Anchor.Centre, sprite.Origin);
@ -97,6 +95,27 @@ namespace osu.Game.Tests.Beatmaps.Formats
} }
} }
[Test]
public void TestLoopWithoutExplicitFadeOut()
{
var decoder = new LegacyStoryboardDecoder();
using (var resStream = TestResources.OpenResource("animation-loop-no-explicit-end-time.osb"))
using (var stream = new LineBufferedReader(resStream))
{
var storyboard = decoder.Decode(stream);
StoryboardLayer background = storyboard.Layers.Single(l => l.Depth == 3);
Assert.AreEqual(1, background.Elements.Count);
Assert.AreEqual(2000, background.Elements[0].StartTime);
Assert.AreEqual(2000, (background.Elements[0] as StoryboardAnimation)?.EarliestTransformTime);
Assert.AreEqual(3000, (background.Elements[0] as StoryboardAnimation)?.GetEndTime());
Assert.AreEqual(12000, (background.Elements[0] as StoryboardAnimation)?.EndTimeForDisplay);
}
}
[Test] [Test]
public void TestCorrectAnimationStartTime() public void TestCorrectAnimationStartTime()
{ {
@ -171,6 +190,55 @@ namespace osu.Game.Tests.Beatmaps.Formats
} }
} }
[Test]
public void TestDecodeVideoWithLowercaseExtension()
{
var decoder = new LegacyStoryboardDecoder();
using (var resStream = TestResources.OpenResource("video-with-lowercase-extension.osb"))
using (var stream = new LineBufferedReader(resStream))
{
var storyboard = decoder.Decode(stream);
StoryboardLayer video = storyboard.Layers.Single(l => l.Name == "Video");
Assert.That(video.Elements.Count, Is.EqualTo(1));
Assert.AreEqual("Video.avi", ((StoryboardVideo)video.Elements[0]).Path);
}
}
[Test]
public void TestDecodeVideoWithUppercaseExtension()
{
var decoder = new LegacyStoryboardDecoder();
using (var resStream = TestResources.OpenResource("video-with-uppercase-extension.osb"))
using (var stream = new LineBufferedReader(resStream))
{
var storyboard = decoder.Decode(stream);
StoryboardLayer video = storyboard.Layers.Single(l => l.Name == "Video");
Assert.That(video.Elements.Count, Is.EqualTo(1));
Assert.AreEqual("Video.AVI", ((StoryboardVideo)video.Elements[0]).Path);
}
}
[Test]
public void TestDecodeImageSpecifiedAsVideo()
{
var decoder = new LegacyStoryboardDecoder();
using (var resStream = TestResources.OpenResource("image-specified-as-video.osb"))
using (var stream = new LineBufferedReader(resStream))
{
var storyboard = decoder.Decode(stream);
StoryboardLayer video = storyboard.Layers.Single(l => l.Name == "Video");
Assert.That(video.Elements.Count, Is.Zero);
}
}
[Test] [Test]
public void TestDecodeOutOfRangeLoopAnimationType() public void TestDecodeOutOfRangeLoopAnimationType()
{ {

View File

@ -0,0 +1,6 @@
[Events]
//Storyboard Layer 0 (Background)
Animation,Background,Centre,"img.jpg",320,240,2,150,LoopForever
F,0,2000,,0,1
L,2000,10
F,18,0,1000,1,0

View File

@ -0,0 +1,4 @@
osu file format v14
[Events]
Video,0,"BG.jpg",0,0

View File

@ -0,0 +1,5 @@
osu file format v14
[Events]
0,0,"BG.jpg",0,0
Video,0,"Video.avi",0,0

View File

@ -0,0 +1,5 @@
osu file format v14
[Events]
0,0,"BG.jpg",0,0
Video,0,"Video.AVI",0,0

View File

@ -164,7 +164,7 @@ namespace osu.Game.Tests.Rulesets
this.parentManager = parentManager; this.parentManager = parentManager;
} }
public override byte[] LoadRaw(string name) => parentManager.LoadRaw(name); public override byte[] GetRawData(string fileName) => parentManager.GetRawData(fileName);
public bool IsDisposed { get; private set; } public bool IsDisposed { get; private set; }

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using NUnit.Framework; using NUnit.Framework;
@ -51,9 +49,11 @@ namespace osu.Game.Tests.Testing
[Test] [Test]
public void TestRetrieveShader() public void TestRetrieveShader()
{ {
AddAssert("ruleset shaders retrieved", () => AddStep("ruleset shaders retrieved without error", () =>
Dependencies.Get<ShaderManager>().LoadRaw(@"sh_TestVertex.vs") != null && {
Dependencies.Get<ShaderManager>().LoadRaw(@"sh_TestFragment.fs") != null); Dependencies.Get<ShaderManager>().GetRawData(@"sh_TestVertex.vs");
Dependencies.Get<ShaderManager>().GetRawData(@"sh_TestFragment.fs");
});
} }
[Test] [Test]
@ -76,12 +76,12 @@ namespace osu.Game.Tests.Testing
} }
public override IResourceStore<byte[]> CreateResourceStore() => new NamespacedResourceStore<byte[]>(TestResources.GetStore(), @"Resources"); public override IResourceStore<byte[]> CreateResourceStore() => new NamespacedResourceStore<byte[]>(TestResources.GetStore(), @"Resources");
public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new TestRulesetConfigManager(); public override IRulesetConfigManager CreateConfig(SettingsStore? settings) => new TestRulesetConfigManager();
public override IEnumerable<Mod> GetModsFor(ModType type) => Array.Empty<Mod>(); public override IEnumerable<Mod> GetModsFor(ModType type) => Array.Empty<Mod>();
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => null; public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod>? mods = null) => null!;
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => null; public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => null!;
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => null; public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => null!;
} }
private class TestRulesetConfigManager : IRulesetConfigManager private class TestRulesetConfigManager : IRulesetConfigManager

View File

@ -311,6 +311,7 @@ namespace osu.Game.Tests.Visual.Background
public bool IsDrawable => true; public bool IsDrawable => true;
public double StartTime => double.MinValue; public double StartTime => double.MinValue;
public double EndTime => double.MaxValue; public double EndTime => double.MaxValue;
public double EndTimeForDisplay => double.MaxValue;
public Drawable CreateDrawable() => new DrawableTestStoryboardElement(); public Drawable CreateDrawable() => new DrawableTestStoryboardElement();
} }

View File

@ -0,0 +1,37 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Extensions;
using osu.Game.Database;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.Editing
{
public partial class TestSceneLocallyModifyingOnlineBeatmaps : EditorSavingTestScene
{
public override void SetUpSteps()
{
CreateInitialBeatmap = () =>
{
var importedSet = Game.BeatmapManager.Import(new ImportTask(TestResources.GetTestBeatmapForImport())).GetResultSafely();
return Game.BeatmapManager.GetWorkingBeatmap(importedSet!.Value.Beatmaps.First());
};
base.SetUpSteps();
}
[Test]
public void TestLocallyModifyingOnlineBeatmap()
{
AddAssert("editor beatmap has online ID", () => EditorBeatmap.BeatmapInfo.OnlineID, () => Is.GreaterThan(0));
AddStep("delete first hitobject", () => EditorBeatmap.RemoveAt(0));
SaveEditor();
ReloadEditorToSameBeatmap();
AddAssert("editor beatmap online ID reset", () => EditorBeatmap.BeatmapInfo.OnlineID, () => Is.EqualTo(-1));
}
}
}

View File

@ -35,14 +35,14 @@ namespace osu.Game.Tests.Visual.Gameplay
var referenceBeatmap = CreateBeatmap(new OsuRuleset().RulesetInfo); var referenceBeatmap = CreateBeatmap(new OsuRuleset().RulesetInfo);
AddUntilStep("score above zero", () => Player.ScoreProcessor.TotalScore.Value > 0); AddUntilStep("score above zero", () => Player.ScoreProcessor.TotalScore.Value > 0);
AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 2)); AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Counters.Any(kc => kc.CountPresses.Value > 2));
seekTo(referenceBeatmap.Breaks[0].StartTime); seekTo(referenceBeatmap.Breaks[0].StartTime);
AddAssert("keys not counting", () => !Player.HUDOverlay.KeyCounter.IsCounting); AddAssert("keys not counting", () => !Player.HUDOverlay.KeyCounter.IsCounting.Value);
AddAssert("overlay displays 100% accuracy", () => Player.BreakOverlay.ChildrenOfType<BreakInfo>().Single().AccuracyDisplay.Current.Value == 1); AddAssert("overlay displays 100% accuracy", () => Player.BreakOverlay.ChildrenOfType<BreakInfo>().Single().AccuracyDisplay.Current.Value == 1);
AddStep("rewind", () => Player.GameplayClockContainer.Seek(-80000)); AddStep("rewind", () => Player.GameplayClockContainer.Seek(-80000));
AddUntilStep("key counter reset", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0)); AddUntilStep("key counter reset", () => Player.HUDOverlay.KeyCounter.Counters.All(kc => kc.CountPresses.Value == 0));
seekTo(referenceBeatmap.HitObjects[^1].GetEndTime()); seekTo(referenceBeatmap.HitObjects[^1].GetEndTime());
AddUntilStep("results displayed", () => getResultsScreen()?.IsLoaded == true); AddUntilStep("results displayed", () => getResultsScreen()?.IsLoaded == true);

View File

@ -31,11 +31,11 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning); AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
addSeekStep(3000); addSeekStep(3000);
AddAssert("all judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => h.Judged)); AddAssert("all judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => h.Judged));
AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Children.Select(kc => kc.CountPresses).Sum() == 15); AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Counters.Select(kc => kc.CountPresses.Value).Sum() == 15);
AddStep("clear results", () => Player.Results.Clear()); AddStep("clear results", () => Player.Results.Clear());
addSeekStep(0); addSeekStep(0);
AddAssert("none judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => !h.Judged)); AddAssert("none judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => !h.Judged));
AddUntilStep("key counters reset", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0)); AddUntilStep("key counters reset", () => Player.HUDOverlay.KeyCounter.Counters.All(kc => kc.CountPresses.Value == 0));
AddAssert("no results triggered", () => Player.Results.Count == 0); AddAssert("no results triggered", () => Player.Results.Count == 0);
} }

View File

@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual.Gameplay
// best way to check without exposing. // best way to check without exposing.
private Drawable hideTarget => hudOverlay.KeyCounter; private Drawable hideTarget => hudOverlay.KeyCounter;
private FillFlowContainer<KeyCounter> keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType<FillFlowContainer<KeyCounter>>().First(); private Drawable keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType<FillFlowContainer<KeyCounter>>().Single();
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
@ -267,7 +267,7 @@ namespace osu.Game.Tests.Visual.Gameplay
hudOverlay = new HUDOverlay(null, Array.Empty<Mod>()); hudOverlay = new HUDOverlay(null, Array.Empty<Mod>());
// Add any key just to display the key counter visually. // Add any key just to display the key counter visually.
hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space)); hudOverlay.KeyCounter.Add(new KeyCounterKeyboardTrigger(Key.Space));
scoreProcessor.Combo.Value = 1; scoreProcessor.Combo.Value = 1;

View File

@ -8,6 +8,8 @@ using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
using osuTK;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
@ -17,43 +19,63 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
public TestSceneKeyCounter() public TestSceneKeyCounter()
{ {
KeyCounterKeyboard testCounter; KeyCounterDisplay defaultDisplay = new DefaultKeyCounterDisplay
KeyCounterDisplay kc = new KeyCounterDisplay
{ {
Origin = Anchor.Centre, Origin = Anchor.Centre,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Children = new KeyCounter[] Position = new Vector2(0, 72.7f)
{
testCounter = new KeyCounterKeyboard(Key.X),
new KeyCounterKeyboard(Key.X),
new KeyCounterMouse(MouseButton.Left),
new KeyCounterMouse(MouseButton.Right),
},
}; };
KeyCounterDisplay argonDisplay = new ArgonKeyCounterDisplay
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Position = new Vector2(0, -72.7f)
};
defaultDisplay.AddRange(new InputTrigger[]
{
new KeyCounterKeyboardTrigger(Key.X),
new KeyCounterKeyboardTrigger(Key.X),
new KeyCounterMouseTrigger(MouseButton.Left),
new KeyCounterMouseTrigger(MouseButton.Right),
});
argonDisplay.AddRange(new InputTrigger[]
{
new KeyCounterKeyboardTrigger(Key.X),
new KeyCounterKeyboardTrigger(Key.X),
new KeyCounterMouseTrigger(MouseButton.Left),
new KeyCounterMouseTrigger(MouseButton.Right),
});
var testCounter = (DefaultKeyCounter)defaultDisplay.Counters.First();
AddStep("Add random", () => AddStep("Add random", () =>
{ {
Key key = (Key)((int)Key.A + RNG.Next(26)); Key key = (Key)((int)Key.A + RNG.Next(26));
kc.Add(new KeyCounterKeyboard(key)); defaultDisplay.Add(new KeyCounterKeyboardTrigger(key));
argonDisplay.Add(new KeyCounterKeyboardTrigger(key));
}); });
Key testKey = ((KeyCounterKeyboard)kc.Children.First()).Key; Key testKey = ((KeyCounterKeyboardTrigger)defaultDisplay.Counters.First().Trigger).Key;
void addPressKeyStep() addPressKeyStep();
AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses.Value == 1);
addPressKeyStep();
AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses.Value == 2);
AddStep("Disable counting", () =>
{ {
AddStep($"Press {testKey} key", () => InputManager.Key(testKey)); argonDisplay.IsCounting.Value = false;
} defaultDisplay.IsCounting.Value = false;
});
addPressKeyStep();
AddAssert($"Check {testKey} count has not changed", () => testCounter.CountPresses.Value == 2);
addPressKeyStep(); Add(defaultDisplay);
AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses == 1); Add(argonDisplay);
addPressKeyStep();
AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses == 2);
AddStep("Disable counting", () => testCounter.IsCounting = false);
addPressKeyStep();
AddAssert($"Check {testKey} count has not changed", () => testCounter.CountPresses == 2);
Add(kc); void addPressKeyStep() => AddStep($"Press {testKey} key", () => InputManager.Key(testKey));
} }
} }
} }

View File

@ -45,6 +45,9 @@ namespace osu.Game.Tests.Visual.Gameplay
[Resolved] [Resolved]
private SessionStatics sessionStatics { get; set; } private SessionStatics sessionStatics { get; set; }
[Resolved]
private OsuConfigManager config { get; set; }
[Cached(typeof(INotificationOverlay))] [Cached(typeof(INotificationOverlay))]
private readonly NotificationOverlay notificationOverlay; private readonly NotificationOverlay notificationOverlay;
@ -317,6 +320,7 @@ namespace osu.Game.Tests.Visual.Gameplay
saveVolumes(); saveVolumes();
setFullVolume(); setFullVolume();
AddStep("enable storyboards", () => config.SetValue(OsuSetting.ShowStoryboard, true));
AddStep("change epilepsy warning", () => epilepsyWarning = warning); AddStep("change epilepsy warning", () => epilepsyWarning = warning);
AddStep("load dummy beatmap", () => resetPlayer(false)); AddStep("load dummy beatmap", () => resetPlayer(false));
@ -333,12 +337,30 @@ namespace osu.Game.Tests.Visual.Gameplay
restoreVolumes(); restoreVolumes();
} }
[Test]
public void TestEpilepsyWarningWithDisabledStoryboard()
{
saveVolumes();
setFullVolume();
AddStep("disable storyboards", () => config.SetValue(OsuSetting.ShowStoryboard, false));
AddStep("change epilepsy warning", () => epilepsyWarning = true);
AddStep("load dummy beatmap", () => resetPlayer(false));
AddUntilStep("wait for current", () => loader.IsCurrentScreen());
AddUntilStep("epilepsy warning absent", () => getWarning() == null);
restoreVolumes();
}
[Test] [Test]
public void TestEpilepsyWarningEarlyExit() public void TestEpilepsyWarningEarlyExit()
{ {
saveVolumes(); saveVolumes();
setFullVolume(); setFullVolume();
AddStep("enable storyboards", () => config.SetValue(OsuSetting.ShowStoryboard, true));
AddStep("set epilepsy warning", () => epilepsyWarning = true); AddStep("set epilepsy warning", () => epilepsyWarning = true);
AddStep("load dummy beatmap", () => resetPlayer(false)); AddStep("load dummy beatmap", () => resetPlayer(false));
@ -449,7 +471,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("click notification", () => notification.TriggerClick()); AddStep("click notification", () => notification.TriggerClick());
} }
private EpilepsyWarning getWarning() => loader.ChildrenOfType<EpilepsyWarning>().SingleOrDefault(); private EpilepsyWarning getWarning() => loader.ChildrenOfType<EpilepsyWarning>().SingleOrDefault(w => w.IsAlive);
private partial class TestPlayerLoader : PlayerLoader private partial class TestPlayerLoader : PlayerLoader
{ {

View File

@ -164,6 +164,36 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("all results reverted", () => playfield.JudgedObjects.Count, () => Is.EqualTo(0)); AddAssert("all results reverted", () => playfield.JudgedObjects.Count, () => Is.EqualTo(0));
} }
[Test]
public void TestRevertNestedObjects()
{
ManualClock clock = null;
var beatmap = new Beatmap();
beatmap.HitObjects.Add(new TestHitObjectWithNested { Duration = 40 });
createTest(beatmap, 10, () => new FramedClock(clock = new ManualClock()));
AddStep("skip to middle of object", () => clock.CurrentTime = (beatmap.HitObjects[0].StartTime + beatmap.HitObjects[0].GetEndTime()) / 2);
AddAssert("2 objects judged", () => playfield.JudgedObjects.Count, () => Is.EqualTo(2));
AddStep("skip to before end of object", () => clock.CurrentTime = beatmap.HitObjects[0].GetEndTime() - 1);
AddAssert("3 objects judged", () => playfield.JudgedObjects.Count, () => Is.EqualTo(3));
DrawableHitObject drawableHitObject = null;
HashSet<HitObject> revertedHitObjects = new HashSet<HitObject>();
AddStep("retrieve drawable hit object", () => drawableHitObject = playfield.ChildrenOfType<DrawableTestHitObjectWithNested>().Single());
AddStep("set up revert tracking", () =>
{
revertedHitObjects.Clear();
drawableHitObject.OnRevertResult += (ho, _) => revertedHitObjects.Add(ho.HitObject);
});
AddStep("skip back to object start", () => clock.CurrentTime = beatmap.HitObjects[0].StartTime);
AddAssert("3 reverts fired", () => revertedHitObjects, () => Has.Count.EqualTo(3));
AddAssert("no objects judged", () => playfield.JudgedObjects.Count, () => Is.EqualTo(0));
}
[Test] [Test]
public void TestApplyHitResultOnKilled() public void TestApplyHitResultOnKilled()
{ {
@ -258,6 +288,8 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
RegisterPool<TestHitObject, DrawableTestHitObject>(poolSize); RegisterPool<TestHitObject, DrawableTestHitObject>(poolSize);
RegisterPool<TestKilledHitObject, DrawableTestKilledHitObject>(poolSize); RegisterPool<TestKilledHitObject, DrawableTestKilledHitObject>(poolSize);
RegisterPool<TestHitObjectWithNested, DrawableTestHitObjectWithNested>(poolSize);
RegisterPool<NestedHitObject, DrawableNestedHitObject>(poolSize);
} }
protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new TestHitObjectLifetimeEntry(hitObject); protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new TestHitObjectLifetimeEntry(hitObject);
@ -388,6 +420,120 @@ namespace osu.Game.Tests.Visual.Gameplay
} }
} }
private class TestHitObjectWithNested : TestHitObject
{
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{
base.CreateNestedHitObjects(cancellationToken);
for (int i = 0; i < 3; ++i)
AddNested(new NestedHitObject { StartTime = (float)Duration * (i + 1) / 4 });
}
}
private class NestedHitObject : ConvertHitObject
{
}
private partial class DrawableTestHitObjectWithNested : DrawableHitObject<TestHitObjectWithNested>
{
private Container nestedContainer;
public DrawableTestHitObjectWithNested()
: base(null)
{
}
[BackgroundDependencyLoader]
private void load()
{
AddRangeInternal(new Drawable[]
{
new Circle
{
RelativeSizeAxes = Axes.Both,
Colour = Colour4.Red
},
nestedContainer = new Container
{
RelativeSizeAxes = Axes.Both
}
});
}
protected override void OnApply()
{
base.OnApply();
Size = new Vector2(200, 50);
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
}
protected override void AddNestedHitObject(DrawableHitObject hitObject)
{
base.AddNestedHitObject(hitObject);
nestedContainer.Add(hitObject);
}
protected override void ClearNestedHitObjects()
{
base.ClearNestedHitObjects();
nestedContainer.Clear(false);
}
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
base.CheckForResult(userTriggered, timeOffset);
if (timeOffset >= 0)
ApplyResult(r => r.Type = r.Judgement.MaxResult);
}
}
private partial class DrawableNestedHitObject : DrawableHitObject<NestedHitObject>
{
public DrawableNestedHitObject()
: this(null)
{
}
public DrawableNestedHitObject(NestedHitObject hitObject)
: base(hitObject)
{
Size = new Vector2(15);
Colour = Colour4.White;
RelativePositionAxes = Axes.Both;
Origin = Anchor.Centre;
}
[BackgroundDependencyLoader]
private void load()
{
AddInternal(new Circle
{
RelativeSizeAxes = Axes.Both,
});
}
protected override void OnApply()
{
base.OnApply();
X = (float)((HitObject.StartTime - ParentHitObject!.HitObject.StartTime) / (ParentHitObject.HitObject.GetEndTime() - ParentHitObject.HitObject.StartTime));
Y = 0.5f;
LifetimeStart = ParentHitObject.LifetimeStart;
LifetimeEnd = ParentHitObject.LifetimeEnd;
}
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
base.CheckForResult(userTriggered, timeOffset);
if (timeOffset >= 0)
ApplyResult(r => r.Type = r.Judgement.MaxResult);
}
}
#endregion #endregion
} }
} }

View File

@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override void AddCheckSteps() protected override void AddCheckSteps()
{ {
AddUntilStep("score above zero", () => ((ScoreAccessibleReplayPlayer)Player).ScoreProcessor.TotalScore.Value > 0); AddUntilStep("score above zero", () => ((ScoreAccessibleReplayPlayer)Player).ScoreProcessor.TotalScore.Value > 0);
AddUntilStep("key counter counted keys", () => ((ScoreAccessibleReplayPlayer)Player).HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 0)); AddUntilStep("key counter counted keys", () => ((ScoreAccessibleReplayPlayer)Player).HUDOverlay.KeyCounter.Counters.Any(kc => kc.CountPresses.Value > 0));
AddAssert("cannot fail", () => !((ScoreAccessibleReplayPlayer)Player).AllowFail); AddAssert("cannot fail", () => !((ScoreAccessibleReplayPlayer)Player).AllowFail);
} }

View File

@ -8,6 +8,7 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Scoring;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Beatmaps;
using osuTK.Input; using osuTK.Input;
@ -45,6 +46,18 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("Time still stopped", () => lastTime == Player.GameplayClockContainer.CurrentTime); AddAssert("Time still stopped", () => lastTime == Player.GameplayClockContainer.CurrentTime);
} }
[Test]
public void TestDoesNotFailOnExit()
{
loadPlayerWithBeatmap();
AddUntilStep("wait for first hit", () => Player.ScoreProcessor.TotalScore.Value > 0);
AddAssert("ensure rank is not fail", () => Player.ScoreProcessor.Rank.Value, () => Is.Not.EqualTo(ScoreRank.F));
AddStep("exit player", () => Player.Exit());
AddUntilStep("wait for exit", () => Player.Parent == null);
AddAssert("ensure rank is not fail", () => Player.ScoreProcessor.Rank.Value, () => Is.Not.EqualTo(ScoreRank.F));
}
[Test] [Test]
public void TestPauseViaSpaceWithSkip() public void TestPauseViaSpaceWithSkip()
{ {

View File

@ -14,6 +14,7 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
using osu.Game.Tests.Gameplay; using osu.Game.Tests.Gameplay;
using osuTK.Input; using osuTK.Input;
@ -57,7 +58,7 @@ namespace osu.Game.Tests.Visual.Gameplay
}; };
// Add any key just to display the key counter visually. // Add any key just to display the key counter visually.
hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space)); hudOverlay.KeyCounter.Add(new KeyCounterKeyboardTrigger(Key.Space));
scoreProcessor.Combo.Value = 1; scoreProcessor.Combo.Value = 1;
return new Container return new Container

View File

@ -18,6 +18,7 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
using osu.Game.Tests.Gameplay; using osu.Game.Tests.Gameplay;
using osuTK.Input; using osuTK.Input;
@ -43,7 +44,7 @@ namespace osu.Game.Tests.Visual.Gameplay
// best way to check without exposing. // best way to check without exposing.
private Drawable hideTarget => hudOverlay.KeyCounter; private Drawable hideTarget => hudOverlay.KeyCounter;
private FillFlowContainer<KeyCounter> keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType<FillFlowContainer<KeyCounter>>().First(); private Drawable keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType<FillFlowContainer<KeyCounter>>().Single();
[Test] [Test]
public void TestComboCounterIncrementing() public void TestComboCounterIncrementing()
@ -88,7 +89,7 @@ namespace osu.Game.Tests.Visual.Gameplay
hudOverlay = new HUDOverlay(null, Array.Empty<Mod>()); hudOverlay = new HUDOverlay(null, Array.Empty<Mod>());
// Add any key just to display the key counter visually. // Add any key just to display the key counter visually.
hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space)); hudOverlay.KeyCounter.Add(new KeyCounterKeyboardTrigger(Key.Space));
action?.Invoke(hudOverlay); action?.Invoke(hudOverlay);

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@ -22,8 +20,10 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
public partial class TestSceneSkinnableSound : OsuTestScene public partial class TestSceneSkinnableSound : OsuTestScene
{ {
private TestSkinSourceContainer skinSource; private TestSkinSourceContainer skinSource = null!;
private PausableSkinnableSound skinnableSound; private PausableSkinnableSound skinnableSound = null!;
private const string sample_lookup = "Gameplay/normal-sliderslide";
[SetUpSteps] [SetUpSteps]
public void SetUpSteps() public void SetUpSteps()
@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.Gameplay
}; };
// has to be added after the hierarchy above else the `ISkinSource` dependency won't be cached. // has to be added after the hierarchy above else the `ISkinSource` dependency won't be cached.
skinSource.Add(skinnableSound = new PausableSkinnableSound(new SampleInfo("Gameplay/normal-sliderslide"))); skinSource.Add(skinnableSound = new PausableSkinnableSound(new SampleInfo(sample_lookup)));
}); });
} }
@ -99,10 +99,28 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("sample not playing", () => !skinnableSound.IsPlaying); AddAssert("sample not playing", () => !skinnableSound.IsPlaying);
} }
[Test]
public void TestSampleUpdatedBeforePlaybackWhenNotPresent()
{
AddStep("make sample non-present", () => skinnableSound.Hide());
AddUntilStep("ensure not present", () => skinnableSound.IsPresent, () => Is.False);
AddUntilStep("ensure sample loaded", () => skinnableSound.ChildrenOfType<DrawableSample>().Single().Name, () => Is.EqualTo(sample_lookup));
AddStep("change source", () =>
{
skinSource.OverridingSample = new SampleVirtual("new skin");
skinSource.TriggerSourceChanged();
});
AddStep("start sample", () => skinnableSound.Play());
AddUntilStep("sample updated", () => skinnableSound.ChildrenOfType<DrawableSample>().Single().Name, () => Is.EqualTo("new skin"));
}
[Test] [Test]
public void TestSkinChangeDoesntPlayOnPause() public void TestSkinChangeDoesntPlayOnPause()
{ {
DrawableSample sample = null; DrawableSample? sample = null;
AddStep("start sample", () => AddStep("start sample", () =>
{ {
skinnableSound.Play(); skinnableSound.Play();
@ -118,7 +136,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("retrieve and ensure current sample is different", () => AddAssert("retrieve and ensure current sample is different", () =>
{ {
DrawableSample oldSample = sample; DrawableSample? oldSample = sample;
sample = skinnableSound.ChildrenOfType<DrawableSample>().Single(); sample = skinnableSound.ChildrenOfType<DrawableSample>().Single();
return sample != oldSample; return sample != oldSample;
}); });
@ -134,20 +152,29 @@ namespace osu.Game.Tests.Visual.Gameplay
private partial class TestSkinSourceContainer : Container, ISkinSource, ISamplePlaybackDisabler private partial class TestSkinSourceContainer : Container, ISkinSource, ISamplePlaybackDisabler
{ {
[Resolved] [Resolved]
private ISkinSource source { get; set; } private ISkinSource source { get; set; } = null!;
public event Action SourceChanged; public event Action? SourceChanged;
public Bindable<bool> SamplePlaybackDisabled { get; } = new Bindable<bool>(); public Bindable<bool> SamplePlaybackDisabled { get; } = new Bindable<bool>();
public ISample? OverridingSample;
IBindable<bool> ISamplePlaybackDisabler.SamplePlaybackDisabled => SamplePlaybackDisabled; IBindable<bool> ISamplePlaybackDisabler.SamplePlaybackDisabled => SamplePlaybackDisabled;
public Drawable GetDrawableComponent(ISkinComponentLookup lookup) => source?.GetDrawableComponent(lookup); public Drawable? GetDrawableComponent(ISkinComponentLookup lookup) => source.GetDrawableComponent(lookup);
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => source?.GetTexture(componentName, wrapModeS, wrapModeT); public Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => source.GetTexture(componentName, wrapModeS, wrapModeT);
public ISample GetSample(ISampleInfo sampleInfo) => source?.GetSample(sampleInfo); public ISample? GetSample(ISampleInfo sampleInfo) => OverridingSample ?? source.GetSample(sampleInfo);
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => source?.GetConfig<TLookup, TValue>(lookup);
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => lookupFunction(this) ? this : source?.FindProvider(lookupFunction); public IBindable<TValue>? GetConfig<TLookup, TValue>(TLookup lookup)
public IEnumerable<ISkin> AllSources => new[] { this }.Concat(source?.AllSources ?? Enumerable.Empty<ISkin>()); where TLookup : notnull
where TValue : notnull
{
return source.GetConfig<TLookup, TValue>(lookup);
}
public ISkin? FindProvider(Func<ISkin, bool> lookupFunction) => lookupFunction(this) ? this : source.FindProvider(lookupFunction);
public IEnumerable<ISkin> AllSources => new[] { this }.Concat(source.AllSources);
public void TriggerSourceChanged() public void TriggerSourceChanged()
{ {

View File

@ -13,6 +13,7 @@ using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.Containers;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
@ -106,6 +107,26 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for fail overlay", () => Player.FailOverlay.State.Value == Visibility.Visible); AddUntilStep("wait for fail overlay", () => Player.FailOverlay.State.Value == Visibility.Visible);
} }
[Test]
public void TestSaveFailedReplayWithStoryboardEndedDoesNotProgress()
{
CreateTest(() =>
{
AddStep("fail on first judgement", () => currentFailConditions = (_, _) => true);
AddStep("set storyboard duration to 0s", () => currentStoryboardDuration = 0);
});
AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.CurrentTime >= currentStoryboardDuration);
AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed);
AddUntilStep("wait for fail overlay", () => Player.FailOverlay.State.Value == Visibility.Visible);
AddUntilStep("wait for button clickable", () => Player.ChildrenOfType<SaveFailedScoreButton>().First().ChildrenOfType<OsuClickableContainer>().First().Enabled.Value);
AddStep("click save button", () => Player.ChildrenOfType<SaveFailedScoreButton>().First().ChildrenOfType<OsuClickableContainer>().First().TriggerClick());
// Test a regression where importing the fail replay would cause progression to results screen in a failed state.
AddWaitStep("wait some", 10);
AddAssert("player is still current screen", () => Player.IsCurrentScreen());
}
[Test] [Test]
public void TestShowResultsFalse() public void TestShowResultsFalse()
{ {

View File

@ -114,6 +114,19 @@ namespace osu.Game.Tests.Visual.Menus
} }
} }
[TestCase(OverlayActivation.All)]
[TestCase(OverlayActivation.Disabled)]
public void TestButtonKeyboardInputRespectsOverlayActivation(OverlayActivation mode)
{
AddStep($"set activation mode to {mode}", () => toolbar.OverlayActivationMode.Value = mode);
AddStep("hide toolbar", () => toolbar.Hide());
if (mode == OverlayActivation.Disabled)
AddAssert("check buttons not accepting input", () => InputManager.NonPositionalInputQueue.OfType<ToolbarButton>().Count(), () => Is.Zero);
else
AddAssert("check buttons accepting input", () => InputManager.NonPositionalInputQueue.OfType<ToolbarButton>().Count(), () => Is.Not.Zero);
}
[TestCase(OverlayActivation.All)] [TestCase(OverlayActivation.All)]
[TestCase(OverlayActivation.Disabled)] [TestCase(OverlayActivation.Disabled)]
public void TestRespectsOverlayActivation(OverlayActivation mode) public void TestRespectsOverlayActivation(OverlayActivation mode)

View File

@ -0,0 +1,42 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Menu;
namespace osu.Game.Tests.Visual.Navigation
{
public partial class TestSceneBeatmapEditorNavigation : OsuGameTestScene
{
/// <summary>
/// When entering the editor, a new beatmap is created as part of the asynchronous load process.
/// This test ensures that in the case of an early exit from the editor (ie. while it's still loading)
/// doesn't leave a dangling beatmap behind.
///
/// This may not fail 100% due to timing, but has a pretty high chance of hitting a failure so works well enough
/// as a test.
/// </summary>
[Test]
public void TestCancelNavigationToEditor()
{
BeatmapSetInfo[] beatmapSets = null!;
AddStep("Fetch initial beatmaps", () => beatmapSets = allBeatmapSets());
AddStep("Set current beatmap to default", () => Game.Beatmap.SetDefault());
AddStep("Push editor loader", () => Game.ScreenStack.Push(new EditorLoader()));
AddUntilStep("Wait for loader current", () => Game.ScreenStack.CurrentScreen is EditorLoader);
AddStep("Close editor while loading", () => Game.ScreenStack.CurrentScreen.Exit());
AddUntilStep("Wait for menu", () => Game.ScreenStack.CurrentScreen is MainMenu);
AddAssert("Check no new beatmaps were made", () => allBeatmapSets().SequenceEqual(beatmapSets));
BeatmapSetInfo[] allBeatmapSets() => Game.Realm.Run(realm => realm.All<BeatmapSetInfo>().Where(x => !x.DeletePending).ToArray());
}
}
}

View File

@ -14,6 +14,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Overlays.Settings.Sections.Input; using osu.Game.Overlays.Settings.Sections.Input;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Select; using osu.Game.Screens.Select;
using osu.Game.Tests.Beatmaps.IO; using osu.Game.Tests.Beatmaps.IO;
using osuTK.Input; using osuTK.Input;
@ -69,10 +70,10 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("wait for gameplay", () => player?.IsBreakTime.Value == false); AddUntilStep("wait for gameplay", () => player?.IsBreakTime.Value == false);
AddStep("press 'z'", () => InputManager.Key(Key.Z)); AddStep("press 'z'", () => InputManager.Key(Key.Z));
AddAssert("key counter didn't increase", () => keyCounter.CountPresses == 0); AddAssert("key counter didn't increase", () => keyCounter.CountPresses.Value == 0);
AddStep("press 's'", () => InputManager.Key(Key.S)); AddStep("press 's'", () => InputManager.Key(Key.S));
AddAssert("key counter did increase", () => keyCounter.CountPresses == 1); AddAssert("key counter did increase", () => keyCounter.CountPresses.Value == 1);
} }
private KeyBindingsSubsection osuBindingSubsection => keyBindingPanel private KeyBindingsSubsection osuBindingSubsection => keyBindingPanel

View File

@ -14,6 +14,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps.Drawables; using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
@ -26,7 +27,7 @@ using APIUser = osu.Game.Online.API.Requests.Responses.APIUser;
namespace osu.Game.Tests.Visual.Online namespace osu.Game.Tests.Visual.Online
{ {
public partial class TestSceneBeatmapSetOverlay : OsuTestScene public partial class TestSceneBeatmapSetOverlay : OsuManualInputManagerTestScene
{ {
private readonly TestBeatmapSetOverlay overlay; private readonly TestBeatmapSetOverlay overlay;
@ -281,6 +282,22 @@ namespace osu.Game.Tests.Visual.Online
AddAssert(@"type is correct", () => type == lookupType.ToString()); AddAssert(@"type is correct", () => type == lookupType.ToString());
} }
[Test]
public void TestBeatmapSetWithGuestDifficulty()
{
AddStep("show map", () => overlay.ShowBeatmapSet(createBeatmapSetWithGuestDifficulty()));
AddStep("move mouse to host difficulty", () =>
{
InputManager.MoveMouseTo(overlay.ChildrenOfType<DifficultyIcon>().ElementAt(0));
});
AddAssert("guest mapper information not shown", () => overlay.ChildrenOfType<BeatmapPicker>().Single().ChildrenOfType<OsuSpriteText>().All(s => s.Text != "BanchoBot"));
AddStep("move mouse to guest difficulty", () =>
{
InputManager.MoveMouseTo(overlay.ChildrenOfType<DifficultyIcon>().ElementAt(1));
});
AddAssert("guest mapper information shown", () => overlay.ChildrenOfType<BeatmapPicker>().Single().ChildrenOfType<OsuSpriteText>().Any(s => s.Text == "BanchoBot"));
}
private APIBeatmapSet createManyDifficultiesBeatmapSet() private APIBeatmapSet createManyDifficultiesBeatmapSet()
{ {
var set = getBeatmapSet(); var set = getBeatmapSet();
@ -320,6 +337,60 @@ namespace osu.Game.Tests.Visual.Online
return beatmapSet; return beatmapSet;
} }
private APIBeatmapSet createBeatmapSetWithGuestDifficulty()
{
var set = getBeatmapSet();
var beatmaps = new List<APIBeatmap>();
var guestUser = new APIUser
{
Username = @"BanchoBot",
Id = 3,
};
set.RelatedUsers = new[]
{
set.Author, guestUser
};
beatmaps.Add(new APIBeatmap
{
OnlineID = 1145,
DifficultyName = "Host Diff",
RulesetID = Ruleset.Value.OnlineID,
StarRating = 1.4,
OverallDifficulty = 3.5f,
AuthorID = set.AuthorID,
FailTimes = new APIFailTimes
{
Fails = Enumerable.Range(1, 100).Select(j => j % 12 - 6).ToArray(),
Retries = Enumerable.Range(-2, 100).Select(j => j % 12 - 6).ToArray(),
},
Status = BeatmapOnlineStatus.Graveyard
});
beatmaps.Add(new APIBeatmap
{
OnlineID = 1919,
DifficultyName = "Guest Diff",
RulesetID = Ruleset.Value.OnlineID,
StarRating = 8.1,
OverallDifficulty = 3.5f,
AuthorID = 3,
FailTimes = new APIFailTimes
{
Fails = Enumerable.Range(1, 100).Select(j => j % 12 - 6).ToArray(),
Retries = Enumerable.Range(-2, 100).Select(j => j % 12 - 6).ToArray(),
},
Status = BeatmapOnlineStatus.Graveyard
});
set.Beatmaps = beatmaps.ToArray();
return set;
}
private void downloadAssert(bool shown) private void downloadAssert(bool shown)
{ {
AddAssert($"is download button {(shown ? "shown" : "hidden")}", () => overlay.Header.HeaderContent.DownloadButtonsVisible == shown); AddAssert($"is download button {(shown ? "shown" : "hidden")}", () => overlay.Header.HeaderContent.DownloadButtonsVisible == shown);

View File

@ -1068,6 +1068,21 @@ namespace osu.Game.Tests.Visual.SongSelect
AddAssert("options disabled", () => !songSelect.ChildrenOfType<FooterButtonOptions>().Single().Enabled.Value); AddAssert("options disabled", () => !songSelect.ChildrenOfType<FooterButtonOptions>().Single().Enabled.Value);
} }
[Test]
public void TestTextBoxBeatmapDifficultyCount()
{
createSongSelect();
AddAssert("0 matching shown", () => songSelect.ChildrenOfType<FilterControl>().Single().InformationalText == "0 matches");
addRulesetImportStep(0);
AddAssert("3 matching shown", () => songSelect.ChildrenOfType<FilterControl>().Single().InformationalText == "3 matches");
AddStep("delete all beatmaps", () => manager.Delete());
AddUntilStep("wait for no beatmap", () => Beatmap.IsDefault);
AddAssert("0 matching shown", () => songSelect.ChildrenOfType<FilterControl>().Single().InformationalText == "0 matches");
}
private void waitForInitialSelection() private void waitForInitialSelection()
{ {
AddUntilStep("wait for initial selection", () => !Beatmap.IsDefault); AddUntilStep("wait for initial selection", () => !Beatmap.IsDefault);

View File

@ -14,10 +14,11 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapListing; using osu.Game.Overlays.BeatmapListing;
using osuTK; using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface namespace osu.Game.Tests.Visual.UserInterface
{ {
public partial class TestSceneBeatmapListingSortTabControl : OsuTestScene public partial class TestSceneBeatmapListingSortTabControl : OsuManualInputManagerTestScene
{ {
private readonly BeatmapListingSortTabControl control; private readonly BeatmapListingSortTabControl control;
@ -111,6 +112,29 @@ namespace osu.Game.Tests.Visual.UserInterface
resetUsesCriteriaOnCategory(SortCriteria.Updated, SearchCategory.Mine); resetUsesCriteriaOnCategory(SortCriteria.Updated, SearchCategory.Mine);
} }
[Test]
public void TestSortDirectionOnCriteriaChange()
{
AddStep("set category to leaderboard", () => control.Reset(SearchCategory.Leaderboard, false));
AddAssert("sort direction is descending", () => control.SortDirection.Value == SortDirection.Descending);
AddStep("click ranked sort button", () =>
{
InputManager.MoveMouseTo(control.TabControl.ChildrenOfType<BeatmapListingSortTabControl.BeatmapTabButton>().Single(s => s.Active.Value));
InputManager.Click(MouseButton.Left);
});
AddAssert("sort direction is ascending", () => control.SortDirection.Value == SortDirection.Ascending);
AddStep("click first inactive sort button", () =>
{
InputManager.MoveMouseTo(control.TabControl.ChildrenOfType<BeatmapListingSortTabControl.BeatmapTabButton>().First(s => !s.Active.Value));
InputManager.Click(MouseButton.Left);
});
AddAssert("sort direction is descending", () => control.SortDirection.Value == SortDirection.Descending);
}
private void criteriaShowsOnCategory(bool expected, SortCriteria criteria, SearchCategory category) private void criteriaShowsOnCategory(bool expected, SortCriteria criteria, SearchCategory category)
{ {
AddAssert($"{criteria.ToString().ToLowerInvariant()} {(expected ? "shown" : "not shown")} on {category.ToString().ToLowerInvariant()}", () => AddAssert($"{criteria.ToString().ToLowerInvariant()} {(expected ? "shown" : "not shown")} on {category.ToString().ToLowerInvariant()}", () =>

View File

@ -35,7 +35,7 @@ namespace osu.Game.Beatmaps
protected override string[] HashableFileTypes => new[] { ".osu" }; protected override string[] HashableFileTypes => new[] { ".osu" };
public Action<(BeatmapSetInfo beatmapSet, bool isBatch)>? ProcessBeatmap { private get; set; } public ProcessBeatmapDelegate? ProcessBeatmap { private get; set; }
public BeatmapImporter(Storage storage, RealmAccess realm) public BeatmapImporter(Storage storage, RealmAccess realm)
: base(storage, realm) : base(storage, realm)
@ -59,7 +59,7 @@ namespace osu.Game.Beatmaps
first.PerformRead(s => first.PerformRead(s =>
{ {
// Re-run processing even in this case. We might have outdated metadata. // Re-run processing even in this case. We might have outdated metadata.
ProcessBeatmap?.Invoke((s, false)); ProcessBeatmap?.Invoke(s, MetadataLookupScope.OnlineFirst);
}); });
return first; return first;
} }
@ -206,7 +206,7 @@ namespace osu.Game.Beatmaps
protected override void PostImport(BeatmapSetInfo model, Realm realm, ImportParameters parameters) protected override void PostImport(BeatmapSetInfo model, Realm realm, ImportParameters parameters)
{ {
base.PostImport(model, realm, parameters); base.PostImport(model, realm, parameters);
ProcessBeatmap?.Invoke((model, parameters.Batch)); ProcessBeatmap?.Invoke(model, parameters.Batch ? MetadataLookupScope.LocalCacheFirst : MetadataLookupScope.OnlineFirst);
} }
private void validateOnlineIds(BeatmapSetInfo beatmapSet, Realm realm) private void validateOnlineIds(BeatmapSetInfo beatmapSet, Realm realm)

View File

@ -167,7 +167,7 @@ namespace osu.Game.Beatmaps
/// </remarks> /// </remarks>
public double DistanceSpacing { get; set; } = 1.0; public double DistanceSpacing { get; set; } = 1.0;
public int BeatDivisor { get; set; } public int BeatDivisor { get; set; } = 4;
public int GridSize { get; set; } public int GridSize { get; set; }

View File

@ -42,7 +42,7 @@ namespace osu.Game.Beatmaps
private readonly WorkingBeatmapCache workingBeatmapCache; private readonly WorkingBeatmapCache workingBeatmapCache;
public Action<(BeatmapSetInfo beatmapSet, bool isBatch)>? ProcessBeatmap { private get; set; } public ProcessBeatmapDelegate? ProcessBeatmap { private get; set; }
public override bool PauseImports public override bool PauseImports
{ {
@ -72,7 +72,7 @@ namespace osu.Game.Beatmaps
BeatmapTrackStore = audioManager.GetTrackStore(userResources); BeatmapTrackStore = audioManager.GetTrackStore(userResources);
beatmapImporter = CreateBeatmapImporter(storage, realm); beatmapImporter = CreateBeatmapImporter(storage, realm);
beatmapImporter.ProcessBeatmap = args => ProcessBeatmap?.Invoke(args); beatmapImporter.ProcessBeatmap = (beatmapSet, scope) => ProcessBeatmap?.Invoke(beatmapSet, scope);
beatmapImporter.PostNotification = obj => PostNotification?.Invoke(obj); beatmapImporter.PostNotification = obj => PostNotification?.Invoke(obj);
workingBeatmapCache = CreateWorkingBeatmapCache(audioManager, gameResources, userResources, defaultBeatmap, host); workingBeatmapCache = CreateWorkingBeatmapCache(audioManager, gameResources, userResources, defaultBeatmap, host);
@ -368,7 +368,7 @@ namespace osu.Game.Beatmaps
// user requested abort // user requested abort
return; return;
var video = b.Files.FirstOrDefault(f => OsuGameBase.VIDEO_EXTENSIONS.Any(ex => f.Filename.EndsWith(ex, StringComparison.Ordinal))); var video = b.Files.FirstOrDefault(f => OsuGameBase.VIDEO_EXTENSIONS.Any(ex => f.Filename.EndsWith(ex, StringComparison.OrdinalIgnoreCase)));
if (video != null) if (video != null)
{ {
@ -415,6 +415,13 @@ namespace osu.Game.Beatmaps
// All changes to metadata are made in the provided beatmapInfo, so this should be copied to the `IBeatmap` before encoding. // All changes to metadata are made in the provided beatmapInfo, so this should be copied to the `IBeatmap` before encoding.
beatmapContent.BeatmapInfo = beatmapInfo; beatmapContent.BeatmapInfo = beatmapInfo;
// Since now this is a locally-modified beatmap, we also set all relevant flags to indicate this.
// Importantly, the `ResetOnlineInfo()` call must happen before encoding, as online ID is encoded into the `.osu` file,
// which influences the beatmap checksums.
beatmapInfo.LastLocalUpdate = DateTimeOffset.Now;
beatmapInfo.Status = BeatmapOnlineStatus.LocallyModified;
beatmapInfo.ResetOnlineInfo();
using (var stream = new MemoryStream()) using (var stream = new MemoryStream())
{ {
using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
@ -438,9 +445,6 @@ namespace osu.Game.Beatmaps
beatmapInfo.MD5Hash = stream.ComputeMD5Hash(); beatmapInfo.MD5Hash = stream.ComputeMD5Hash();
beatmapInfo.Hash = stream.ComputeSHA2Hash(); beatmapInfo.Hash = stream.ComputeSHA2Hash();
beatmapInfo.LastLocalUpdate = DateTimeOffset.Now;
beatmapInfo.Status = BeatmapOnlineStatus.LocallyModified;
AddFile(setInfo, stream, createBeatmapFilenameFromMetadata(beatmapInfo)); AddFile(setInfo, stream, createBeatmapFilenameFromMetadata(beatmapInfo));
updateHashAndMarkDirty(setInfo); updateHashAndMarkDirty(setInfo);
@ -454,7 +458,9 @@ namespace osu.Game.Beatmaps
if (transferCollections) if (transferCollections)
beatmapInfo.TransferCollectionReferences(r, oldMd5Hash); beatmapInfo.TransferCollectionReferences(r, oldMd5Hash);
ProcessBeatmap?.Invoke((liveBeatmapSet, false)); // do not look up metadata.
// this is a locally-modified set now, so looking up metadata is busy work at best and harmful at worst.
ProcessBeatmap?.Invoke(liveBeatmapSet, MetadataLookupScope.None);
}); });
} }
@ -542,4 +548,11 @@ namespace osu.Game.Beatmaps
public override string HumanisedModelName => "beatmap"; public override string HumanisedModelName => "beatmap";
} }
/// <summary>
/// Delegate type for beatmap processing callbacks.
/// </summary>
/// <param name="beatmapSet">The beatmap set to be processed.</param>
/// <param name="lookupScope">The scope to use when looking up metadata.</param>
public delegate void ProcessBeatmapDelegate(BeatmapSetInfo beatmapSet, MetadataLookupScope lookupScope);
} }

View File

@ -36,7 +36,7 @@ namespace osu.Game.Beatmaps
var matchingSet = r.All<BeatmapSetInfo>().FirstOrDefault(s => s.OnlineID == id); var matchingSet = r.All<BeatmapSetInfo>().FirstOrDefault(s => s.OnlineID == id);
if (matchingSet != null) if (matchingSet != null)
beatmapUpdater.Queue(matchingSet.ToLive(realm), true); beatmapUpdater.Queue(matchingSet.ToLive(realm), MetadataLookupScope.OnlineFirst);
} }
}); });
} }

View File

@ -42,24 +42,25 @@ namespace osu.Game.Beatmaps
/// Queue a beatmap for background processing. /// Queue a beatmap for background processing.
/// </summary> /// </summary>
/// <param name="beatmapSet">The managed beatmap set to update. A transaction will be opened to apply changes.</param> /// <param name="beatmapSet">The managed beatmap set to update. A transaction will be opened to apply changes.</param>
/// <param name="preferOnlineFetch">Whether metadata from an online source should be preferred. If <c>true</c>, the local cache will be skipped to ensure the freshest data state possible.</param> /// <param name="lookupScope">The preferred scope to use for metadata lookup.</param>
public void Queue(Live<BeatmapSetInfo> beatmapSet, bool preferOnlineFetch = false) public void Queue(Live<BeatmapSetInfo> beatmapSet, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst)
{ {
Logger.Log($"Queueing change for local beatmap {beatmapSet}"); Logger.Log($"Queueing change for local beatmap {beatmapSet}");
Task.Factory.StartNew(() => beatmapSet.PerformRead(b => Process(b, preferOnlineFetch)), default, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); Task.Factory.StartNew(() => beatmapSet.PerformRead(b => Process(b, lookupScope)), default, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler);
} }
/// <summary> /// <summary>
/// Run all processing on a beatmap immediately. /// Run all processing on a beatmap immediately.
/// </summary> /// </summary>
/// <param name="beatmapSet">The managed beatmap set to update. A transaction will be opened to apply changes.</param> /// <param name="beatmapSet">The managed beatmap set to update. A transaction will be opened to apply changes.</param>
/// <param name="preferOnlineFetch">Whether metadata from an online source should be preferred. If <c>true</c>, the local cache will be skipped to ensure the freshest data state possible.</param> /// <param name="lookupScope">The preferred scope to use for metadata lookup.</param>
public void Process(BeatmapSetInfo beatmapSet, bool preferOnlineFetch = false) => beatmapSet.Realm.Write(r => public void Process(BeatmapSetInfo beatmapSet, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst) => beatmapSet.Realm.Write(r =>
{ {
// Before we use below, we want to invalidate. // Before we use below, we want to invalidate.
workingBeatmapCache.Invalidate(beatmapSet); workingBeatmapCache.Invalidate(beatmapSet);
metadataLookup.Update(beatmapSet, preferOnlineFetch); if (lookupScope != MetadataLookupScope.None)
metadataLookup.Update(beatmapSet, lookupScope == MetadataLookupScope.OnlineFirst);
foreach (var beatmap in beatmapSet.Beatmaps) foreach (var beatmap in beatmapSet.Beatmaps)
{ {

View File

@ -363,6 +363,19 @@ namespace osu.Game.Beatmaps.Formats
beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[3]); beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[3]);
break; break;
case LegacyEventType.Video:
string filename = CleanFilename(split[2]);
// Some very old beatmaps had incorrect type specifications for their backgrounds (ie. using 1 for VIDEO
// instead of 0 for BACKGROUND). To handle this gracefully, check the file extension against known supported
// video extensions and handle similar to a background if it doesn't match.
if (!OsuGameBase.VIDEO_EXTENSIONS.Contains(Path.GetExtension(filename).ToLowerInvariant()))
{
beatmap.BeatmapInfo.Metadata.BackgroundFile = filename;
}
break;
case LegacyEventType.Background: case LegacyEventType.Background:
beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[2]); beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[2]);
break; break;

View File

@ -109,6 +109,14 @@ namespace osu.Game.Beatmaps.Formats
int offset = Parsing.ParseInt(split[1]); int offset = Parsing.ParseInt(split[1]);
string path = CleanFilename(split[2]); string path = CleanFilename(split[2]);
// See handling in LegacyBeatmapDecoder for the special case where a video type is used but
// the file extension is not a valid video.
//
// This avoids potential weird crashes when ffmpeg attempts to parse an image file as a video
// (see https://github.com/ppy/osu/issues/22829#issuecomment-1465552451).
if (!OsuGameBase.VIDEO_EXTENSIONS.Contains(Path.GetExtension(path).ToLowerInvariant()))
break;
storyboard.GetLayer("Video").Add(new StoryboardVideo(path, offset)); storyboard.GetLayer("Video").Add(new StoryboardVideo(path, offset));
break; break;
} }
@ -276,7 +284,8 @@ namespace osu.Game.Beatmaps.Formats
switch (type) switch (type)
{ {
case "A": case "A":
timelineGroup?.BlendingParameters.Add(easing, startTime, endTime, BlendingParameters.Additive, startTime == endTime ? BlendingParameters.Additive : BlendingParameters.Inherit); timelineGroup?.BlendingParameters.Add(easing, startTime, endTime, BlendingParameters.Additive,
startTime == endTime ? BlendingParameters.Additive : BlendingParameters.Inherit);
break; break;
case "H": case "H":

View File

@ -0,0 +1,26 @@
// 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.Beatmaps
{
/// <summary>
/// Determines which sources (if any at all) should be queried in which order for a beatmap's metadata.
/// </summary>
public enum MetadataLookupScope
{
/// <summary>
/// Do not attempt to look up the beatmap metadata either in the local cache or online.
/// </summary>
None,
/// <summary>
/// Try the local metadata cache first before querying online sources.
/// </summary>
LocalCacheFirst,
/// <summary>
/// Query online sources immediately.
/// </summary>
OnlineFirst
}
}

View File

@ -70,7 +70,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
{ {
get get
{ {
if (OsuGameBase.VIDEO_EXTENSIONS.Contains(File.Extension)) if (OsuGameBase.VIDEO_EXTENSIONS.Contains(File.Extension.ToLowerInvariant()))
return FontAwesome.Regular.FileVideo; return FontAwesome.Regular.FileVideo;
switch (File.Extension) switch (File.Extension)

View File

@ -99,6 +99,16 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString TimelineTicks => new TranslatableString(getKey(@"timeline_ticks"), @"Ticks"); public static LocalisableString TimelineTicks => new TranslatableString(getKey(@"timeline_ticks"), @"Ticks");
/// <summary>
/// "{0:0}&#176;"
/// </summary>
public static LocalisableString RotationUnsnapped(float newRotation) => new TranslatableString(getKey(@"rotation_unsnapped"), @"{0:0}°", newRotation);
/// <summary>
/// "{0:0}&#176; (snapped)"
/// </summary>
public static LocalisableString RotationSnapped(float newRotation) => new TranslatableString(getKey(@"rotation_snapped"), @"{0:0}° (snapped)", newRotation);
private static string getKey(string key) => $@"{prefix}:{key}"; private static string getKey(string key) => $@"{prefix}:{key}";
} }
} }

View File

@ -15,9 +15,9 @@ namespace osu.Game.Localisation
public static LocalisableString Header => new TranslatableString(getKey(@"header"), @"Obtaining Beatmaps"); public static LocalisableString Header => new TranslatableString(getKey(@"header"), @"Obtaining Beatmaps");
/// <summary> /// <summary>
/// "&quot;Beatmaps&quot; are what we call playable levels. osu! doesn&#39;t come with any beatmaps pre-loaded. This step will help you get started on your beatmap collection." /// "&quot;Beatmaps&quot; are what we call sets of playable levels. osu! doesn&#39;t come with any beatmaps pre-loaded. This step will help you get started on your beatmap collection."
/// </summary> /// </summary>
public static LocalisableString Description => new TranslatableString(getKey(@"description"), @"""Beatmaps"" are what we call playable levels. osu! doesn't come with any beatmaps pre-loaded. This step will help you get started on your beatmap collection."); public static LocalisableString Description => new TranslatableString(getKey(@"description"), @"""Beatmaps"" are what we call sets of playable levels. osu! doesn't come with any beatmaps pre-loaded. This step will help you get started on your beatmap collection.");
/// <summary> /// <summary>
/// "If you are a new player, we recommend playing through the tutorial to get accustomed to the gameplay." /// "If you are a new player, we recommend playing through the tutorial to get accustomed to the gameplay."

View File

@ -50,16 +50,18 @@ namespace osu.Game.Localisation
public static LocalisableString NoAutoplayMod => new TranslatableString(getKey(@"no_autoplay_mod"), @"The current ruleset doesn't have an autoplay mod available!"); public static LocalisableString NoAutoplayMod => new TranslatableString(getKey(@"no_autoplay_mod"), @"The current ruleset doesn't have an autoplay mod available!");
/// <summary> /// <summary>
/// "osu! doesn&#39;t seem to be able to play audio correctly.\n\nPlease try changing your audio device to a working setting." /// "osu! doesn&#39;t seem to be able to play audio correctly.
///
/// Please try changing your audio device to a working setting."
/// </summary> /// </summary>
public static LocalisableString AudioPlaybackIssue => new TranslatableString(getKey(@"audio_playback_issue"), public static LocalisableString AudioPlaybackIssue => new TranslatableString(getKey(@"audio_playback_issue"), @"osu! doesn't seem to be able to play audio correctly.
@"osu! doesn't seem to be able to play audio correctly.\n\nPlease try changing your audio device to a working setting.");
Please try changing your audio device to a working setting.");
/// <summary> /// <summary>
/// "The score overlay is currently disabled. You can toggle this by pressing {0}." /// "The score overlay is currently disabled. You can toggle this by pressing {0}."
/// </summary> /// </summary>
public static LocalisableString ScoreOverlayDisabled(LocalisableString arg0) => new TranslatableString(getKey(@"score_overlay_disabled"), public static LocalisableString ScoreOverlayDisabled(LocalisableString arg0) => new TranslatableString(getKey(@"score_overlay_disabled"), @"The score overlay is currently disabled. You can toggle this by pressing {0}.", arg0);
@"The score overlay is currently disabled. You can toggle this by pressing {0}.", arg0);
private static string getKey(string key) => $@"{prefix}:{key}"; private static string getKey(string key) => $@"{prefix}:{key}";
} }

View File

@ -65,6 +65,11 @@ namespace osu.Game.Localisation
if (manager == null) if (manager == null)
return null; return null;
// When using the English culture, prefer the fallbacks rather than osu-resources baked strings.
// They are guaranteed to be up-to-date, and is also what a developer expects to see when making changes to `xxxStrings.cs` files.
if (EffectiveCulture.Name == @"en")
return null;
try try
{ {
return manager.GetString(key, EffectiveCulture); return manager.GetString(key, EffectiveCulture);

View File

@ -29,6 +29,11 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString CursorTrail => new TranslatableString(getKey(@"cursor_trail"), @"Cursor trail"); public static LocalisableString CursorTrail => new TranslatableString(getKey(@"cursor_trail"), @"Cursor trail");
/// <summary>
/// "Cursor ripples"
/// </summary>
public static LocalisableString CursorRipples => new TranslatableString(getKey(@"cursor_ripples"), @"Cursor ripples");
/// <summary> /// <summary>
/// "Playfield border style" /// "Playfield border style"
/// </summary> /// </summary>

View File

@ -71,7 +71,7 @@ namespace osu.Game
[Cached(typeof(OsuGameBase))] [Cached(typeof(OsuGameBase))]
public partial class OsuGameBase : Framework.Game, ICanAcceptFiles, IBeatSyncProvider public partial class OsuGameBase : Framework.Game, ICanAcceptFiles, IBeatSyncProvider
{ {
public static readonly string[] VIDEO_EXTENSIONS = { ".mp4", ".mov", ".avi", ".flv" }; public static readonly string[] VIDEO_EXTENSIONS = { ".mp4", ".mov", ".avi", ".flv", ".mpg", ".wmv", ".m4v" };
public const string OSU_PROTOCOL = "osu://"; public const string OSU_PROTOCOL = "osu://";
@ -310,7 +310,7 @@ namespace osu.Game
base.Content.Add(new BeatmapOnlineChangeIngest(beatmapUpdater, realm, metadataClient)); base.Content.Add(new BeatmapOnlineChangeIngest(beatmapUpdater, realm, metadataClient));
BeatmapManager.ProcessBeatmap = args => beatmapUpdater.Process(args.beatmapSet, !args.isBatch); BeatmapManager.ProcessBeatmap = (beatmapSet, scope) => beatmapUpdater.Process(beatmapSet, scope);
dependencies.Cache(userCache = new UserLookupCache()); dependencies.Cache(userCache = new UserLookupCache());
base.Content.Add(userCache); base.Content.Add(userCache);

View File

@ -209,7 +209,7 @@ namespace osu.Game.Overlays.AccountCreation
if (!string.IsNullOrEmpty(errors.Message)) if (!string.IsNullOrEmpty(errors.Message))
passwordDescription.AddErrors(new[] { errors.Message }); passwordDescription.AddErrors(new[] { errors.Message });
game.OpenUrlExternally($"{errors.Redirect}?username={usernameTextBox.Text}&email={emailTextBox.Text}"); game.OpenUrlExternally($"{errors.Redirect}?username={usernameTextBox.Text}&email={emailTextBox.Text}", true);
} }
} }
else else

View File

@ -3,6 +3,7 @@
#nullable disable #nullable disable
using osu.Framework.Graphics;
using osu.Game.Localisation; using osu.Game.Localisation;
using osu.Game.Resources.Localisation.Web; using osu.Game.Resources.Localisation.Web;
@ -10,8 +11,12 @@ namespace osu.Game.Overlays.BeatmapListing
{ {
public partial class BeatmapListingHeader : OverlayHeader public partial class BeatmapListingHeader : OverlayHeader
{ {
public BeatmapListingFilterControl FilterControl { get; private set; }
protected override OverlayTitle CreateTitle() => new BeatmapListingTitle(); protected override OverlayTitle CreateTitle() => new BeatmapListingTitle();
protected override Drawable CreateContent() => FilterControl = new BeatmapListingFilterControl();
private partial class BeatmapListingTitle : OverlayTitle private partial class BeatmapListingTitle : OverlayTitle
{ {
public BeatmapListingTitle() public BeatmapListingTitle()

View File

@ -107,7 +107,7 @@ namespace osu.Game.Overlays.BeatmapListing
Padding = new MarginPadding Padding = new MarginPadding
{ {
Vertical = 20, Vertical = 20,
Horizontal = 40, Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING,
}, },
Child = new FillFlowContainer Child = new FillFlowContainer
{ {

View File

@ -25,6 +25,8 @@ namespace osu.Game.Overlays.BeatmapListing
if (currentParameters == null) if (currentParameters == null)
Reset(SearchCategory.Leaderboard, false); Reset(SearchCategory.Leaderboard, false);
Current.BindValueChanged(_ => SortDirection.Value = Overlays.SortDirection.Descending);
} }
public void Reset(SearchCategory category, bool hasQuery) public void Reset(SearchCategory category, bool hasQuery)
@ -102,7 +104,7 @@ namespace osu.Game.Overlays.BeatmapListing
}; };
} }
private partial class BeatmapTabButton : TabButton public partial class BeatmapTabButton : TabButton
{ {
public readonly Bindable<SortDirection> SortDirection = new Bindable<SortDirection>(); public readonly Bindable<SortDirection> SortDirection = new Bindable<SortDirection>();
@ -136,7 +138,7 @@ namespace osu.Game.Overlays.BeatmapListing
SortDirection.BindValueChanged(direction => SortDirection.BindValueChanged(direction =>
{ {
icon.Icon = direction.NewValue == Overlays.SortDirection.Ascending ? FontAwesome.Solid.CaretUp : FontAwesome.Solid.CaretDown; icon.Icon = direction.NewValue == Overlays.SortDirection.Ascending && Active.Value ? FontAwesome.Solid.CaretUp : FontAwesome.Solid.CaretDown;
}, true); }, true);
} }

View File

@ -43,7 +43,8 @@ namespace osu.Game.Overlays
private Container panelTarget; private Container panelTarget;
private FillFlowContainer<BeatmapCard> foundContent; private FillFlowContainer<BeatmapCard> foundContent;
private BeatmapListingFilterControl filterControl;
private BeatmapListingFilterControl filterControl => Header.FilterControl;
public BeatmapListingOverlay() public BeatmapListingOverlay()
: base(OverlayColourScheme.Blue) : base(OverlayColourScheme.Blue)
@ -60,12 +61,6 @@ namespace osu.Game.Overlays
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
Children = new Drawable[] Children = new Drawable[]
{ {
filterControl = new BeatmapListingFilterControl
{
TypingStarted = onTypingStarted,
SearchStarted = onSearchStarted,
SearchFinished = onSearchFinished,
},
new Container new Container
{ {
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
@ -88,6 +83,10 @@ namespace osu.Game.Overlays
}, },
} }
}; };
filterControl.TypingStarted = onTypingStarted;
filterControl.SearchStarted = onSearchStarted;
filterControl.SearchFinished = onSearchFinished;
} }
protected override void LoadComplete() protected override void LoadComplete()

View File

@ -31,6 +31,7 @@ namespace osu.Game.Overlays.BeatmapSet
private const float tile_spacing = 2; private const float tile_spacing = 2;
private readonly OsuSpriteText version, starRating, starRatingText; private readonly OsuSpriteText version, starRating, starRatingText;
private readonly LinkFlowContainer guestMapperContainer;
private readonly FillFlowContainer starRatingContainer; private readonly FillFlowContainer starRatingContainer;
private readonly Statistic plays, favourites; private readonly Statistic plays, favourites;
@ -88,6 +89,14 @@ namespace osu.Game.Overlays.BeatmapSet
Origin = Anchor.BottomLeft, Origin = Anchor.BottomLeft,
Font = OsuFont.GetFont(size: 17, weight: FontWeight.Bold) Font = OsuFont.GetFont(size: 17, weight: FontWeight.Bold)
}, },
guestMapperContainer = new LinkFlowContainer(s =>
s.Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 11))
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Margin = new MarginPadding { Bottom = 1 },
},
starRatingContainer = new FillFlowContainer starRatingContainer = new FillFlowContainer
{ {
Anchor = Anchor.BottomLeft, Anchor = Anchor.BottomLeft,
@ -198,8 +207,21 @@ namespace osu.Game.Overlays.BeatmapSet
updateDifficultyButtons(); updateDifficultyButtons();
} }
private void showBeatmap(IBeatmapInfo? beatmapInfo) private void showBeatmap(APIBeatmap? beatmapInfo)
{ {
guestMapperContainer.Clear();
if (beatmapInfo?.AuthorID != BeatmapSet?.AuthorID)
{
APIUser? user = BeatmapSet?.RelatedUsers?.SingleOrDefault(u => u.OnlineID == beatmapInfo?.AuthorID);
if (user != null)
{
guestMapperContainer.AddText("mapped by ");
guestMapperContainer.AddUserLink(user);
}
}
version.Text = beatmapInfo?.DifficultyName ?? string.Empty; version.Text = beatmapInfo?.DifficultyName ?? string.Empty;
} }

View File

@ -97,8 +97,8 @@ namespace osu.Game.Overlays.BeatmapSet
Padding = new MarginPadding Padding = new MarginPadding
{ {
Vertical = BeatmapSetOverlay.Y_PADDING, Vertical = BeatmapSetOverlay.Y_PADDING,
Left = BeatmapSetOverlay.X_PADDING, Left = WaveOverlayContainer.HORIZONTAL_PADDING,
Right = BeatmapSetOverlay.X_PADDING + BeatmapSetOverlay.RIGHT_WIDTH, Right = WaveOverlayContainer.HORIZONTAL_PADDING + BeatmapSetOverlay.RIGHT_WIDTH,
}, },
Children = new Drawable[] Children = new Drawable[]
{ {
@ -170,7 +170,7 @@ namespace osu.Game.Overlays.BeatmapSet
Anchor = Anchor.BottomRight, Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight, Origin = Anchor.BottomRight,
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Margin = new MarginPadding { Top = BeatmapSetOverlay.Y_PADDING, Right = BeatmapSetOverlay.X_PADDING }, Margin = new MarginPadding { Top = BeatmapSetOverlay.Y_PADDING, Right = WaveOverlayContainer.HORIZONTAL_PADDING },
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
Spacing = new Vector2(10), Spacing = new Vector2(10),
Children = new Drawable[] Children = new Drawable[]

View File

@ -55,7 +55,7 @@ namespace osu.Game.Overlays.BeatmapSet
new Container new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Top = 15, Horizontal = BeatmapSetOverlay.X_PADDING }, Padding = new MarginPadding { Top = 15, Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING },
Children = new Drawable[] Children = new Drawable[]
{ {
new Container new Container

View File

@ -116,7 +116,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
Padding = new MarginPadding { Horizontal = 50 }, Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING },
Margin = new MarginPadding { Vertical = 20 }, Margin = new MarginPadding { Vertical = 20 },
Children = new Drawable[] Children = new Drawable[]
{ {

View File

@ -25,7 +25,6 @@ namespace osu.Game.Overlays
{ {
public partial class BeatmapSetOverlay : OnlineOverlay<BeatmapSetHeader> public partial class BeatmapSetOverlay : OnlineOverlay<BeatmapSetHeader>
{ {
public const float X_PADDING = 40;
public const float Y_PADDING = 25; public const float Y_PADDING = 25;
public const float RIGHT_WIDTH = 275; public const float RIGHT_WIDTH = 275;

View File

@ -18,8 +18,6 @@ namespace osu.Game.Overlays.Changelog
{ {
public partial class ChangelogBuild : FillFlowContainer public partial class ChangelogBuild : FillFlowContainer
{ {
public const float HORIZONTAL_PADDING = 70;
public Action<APIChangelogBuild> SelectBuild; public Action<APIChangelogBuild> SelectBuild;
protected readonly APIChangelogBuild Build; protected readonly APIChangelogBuild Build;
@ -33,7 +31,7 @@ namespace osu.Game.Overlays.Changelog
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y;
Direction = FillDirection.Vertical; Direction = FillDirection.Vertical;
Padding = new MarginPadding { Horizontal = HORIZONTAL_PADDING }; Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING };
Children = new Drawable[] Children = new Drawable[]
{ {

View File

@ -93,7 +93,7 @@ namespace osu.Game.Overlays.Changelog
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Padding = new MarginPadding Padding = new MarginPadding
{ {
Horizontal = 65, Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING - ChangelogUpdateStreamItem.PADDING,
Vertical = 20 Vertical = 20
}, },
Child = Streams = new ChangelogUpdateStreamControl { Current = currentStream }, Child = Streams = new ChangelogUpdateStreamControl { Current = currentStream },

View File

@ -64,7 +64,7 @@ namespace osu.Game.Overlays.Changelog
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Height = 1, Height = 1,
Padding = new MarginPadding { Horizontal = ChangelogBuild.HORIZONTAL_PADDING }, Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING },
Margin = new MarginPadding { Top = 30 }, Margin = new MarginPadding { Top = 30 },
Child = new Box Child = new Box
{ {

View File

@ -34,7 +34,7 @@ namespace osu.Game.Overlays.Changelog
Padding = new MarginPadding Padding = new MarginPadding
{ {
Vertical = 20, Vertical = 20,
Horizontal = 50, Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING,
}; };
} }
@ -79,7 +79,7 @@ namespace osu.Game.Overlays.Changelog
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
Padding = new MarginPadding { Right = 50 + image_container_width }, Padding = new MarginPadding { Right = WaveOverlayContainer.HORIZONTAL_PADDING + image_container_width },
Children = new Drawable[] Children = new Drawable[]
{ {
new OsuSpriteText new OsuSpriteText

View File

@ -99,7 +99,7 @@ namespace osu.Game.Overlays.Comments
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Horizontal = 50, Vertical = 20 }, Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING, Vertical = 20 },
Children = new Drawable[] Children = new Drawable[]
{ {
avatar = new UpdateableAvatar(api.LocalUser.Value) avatar = new UpdateableAvatar(api.LocalUser.Value)
@ -152,7 +152,7 @@ namespace osu.Game.Overlays.Comments
ShowDeleted = { BindTarget = ShowDeleted }, ShowDeleted = { BindTarget = ShowDeleted },
Margin = new MarginPadding Margin = new MarginPadding
{ {
Horizontal = 70, Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING,
Vertical = 10 Vertical = 10
} }
}, },
@ -393,7 +393,7 @@ namespace osu.Game.Overlays.Comments
{ {
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
Margin = new MarginPadding { Left = 50 }, Margin = new MarginPadding { Left = WaveOverlayContainer.HORIZONTAL_PADDING },
Text = CommentsStrings.Empty Text = CommentsStrings.Empty
} }
}); });

View File

@ -41,7 +41,7 @@ namespace osu.Game.Overlays.Comments
new Container new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Horizontal = 50 }, Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING },
Children = new Drawable[] Children = new Drawable[]
{ {
new OverlaySortTabControl<CommentsSortCriteria> new OverlaySortTabControl<CommentsSortCriteria>

View File

@ -537,7 +537,7 @@ namespace osu.Game.Overlays.Comments
{ {
return new MarginPadding return new MarginPadding
{ {
Horizontal = 70, Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING,
Vertical = 15 Vertical = 15
}; };
} }

View File

@ -32,7 +32,7 @@ namespace osu.Game.Overlays.Comments
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal, Direction = FillDirection.Horizontal,
Margin = new MarginPadding { Left = 50 }, Margin = new MarginPadding { Left = WaveOverlayContainer.HORIZONTAL_PADDING },
Spacing = new Vector2(5, 0), Spacing = new Vector2(5, 0),
Children = new Drawable[] Children = new Drawable[]
{ {

View File

@ -132,11 +132,10 @@ namespace osu.Game.Overlays.Comments
}, },
sideNumber = new OsuSpriteText sideNumber = new OsuSpriteText
{ {
Anchor = Anchor.CentreLeft, Anchor = Anchor.TopCentre,
Origin = Anchor.CentreRight, Origin = Anchor.BottomCentre,
Text = "+1", Text = "+1",
Font = OsuFont.GetFont(size: 14), Font = OsuFont.GetFont(size: 14),
Margin = new MarginPadding { Right = 3 },
Alpha = 0, Alpha = 0,
}, },
votesCounter = new OsuSpriteText votesCounter = new OsuSpriteText
@ -189,7 +188,7 @@ namespace osu.Game.Overlays.Comments
else else
sideNumber.FadeTo(IsHovered ? 1 : 0); sideNumber.FadeTo(IsHovered ? 1 : 0);
borderContainer.BorderThickness = IsHovered ? 3 : 0; borderContainer.BorderThickness = IsHovered ? 2 : 0;
} }
private void onHoverAction() private void onHoverAction()

View File

@ -60,7 +60,7 @@ namespace osu.Game.Overlays.Dashboard
new Container<BasicSearchTextBox> new Container<BasicSearchTextBox>
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Padding = new MarginPadding(padding), Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING, Vertical = padding },
Child = searchTextBox = new BasicSearchTextBox Child = searchTextBox = new BasicSearchTextBox
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,

View File

@ -79,7 +79,7 @@ namespace osu.Game.Overlays.Dashboard.Friends
Padding = new MarginPadding Padding = new MarginPadding
{ {
Top = 20, Top = 20,
Horizontal = 45 Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING - FriendsOnlineStatusItem.PADDING
}, },
Child = onlineStreamControl = new FriendOnlineStreamControl(), Child = onlineStreamControl = new FriendOnlineStreamControl(),
} }
@ -129,7 +129,7 @@ namespace osu.Game.Overlays.Dashboard.Friends
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Horizontal = 50 } Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING }
}, },
loading = new LoadingLayer(true) loading = new LoadingLayer(true)
} }

View File

@ -43,7 +43,7 @@ namespace osu.Game.Overlays.News.Displays
{ {
Vertical = 20, Vertical = 20,
Left = 30, Left = 30,
Right = 50 Right = WaveOverlayContainer.HORIZONTAL_PADDING
}; };
InternalChild = new FillFlowContainer InternalChild = new FillFlowContainer

Some files were not shown because too many files have changed in this diff Show More