1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-14 19:22:56 +08:00

Merge branch 'master' into taiko-drumroll-party

This commit is contained in:
Tim Oliver 2020-04-12 15:36:07 +08:00
commit 412ac6121f
78 changed files with 1431 additions and 408 deletions

View File

@ -52,6 +52,6 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.403.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.407.1" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.411.0" />
</ItemGroup>
</Project>

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -31,6 +31,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
typeof(ManiaRuleset),
typeof(ManiaLegacySkinTransformer),
typeof(ManiaSettingsSubsection)
};
protected override Ruleset CreateRulesetForSkinProvider() => new ManiaRuleset();

View File

@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
return new ManiaInputManager(new ManiaRuleset().RulesetInfo, 4)
{
Child = new ManiaStage(0, new StageDefinition { Columns = 4 }, ref normalAction, ref specialAction)
Child = new Stage(0, new StageDefinition { Columns = 4 }, ref normalAction, ref specialAction)
};
});
}

View File

@ -0,0 +1,35 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Mania.Skinning;
using osu.Game.Rulesets.Mania.UI.Components;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
public class TestSceneStageBackground : ManiaSkinnableTestScene
{
public override IReadOnlyList<Type> RequiredTypes => base.RequiredTypes.Concat(new[]
{
typeof(DefaultStageBackground),
typeof(LegacyStageBackground),
}).ToList();
[BackgroundDependencyLoader]
private void load()
{
SetContents(() => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground), _ => new DefaultStageBackground())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Width = 0.5f,
});
}
}
}

View File

@ -0,0 +1,33 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Mania.Skinning;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
public class TestSceneStageForeground : ManiaSkinnableTestScene
{
public override IReadOnlyList<Type> RequiredTypes => base.RequiredTypes.Concat(new[]
{
typeof(LegacyStageForeground),
}).ToList();
[BackgroundDependencyLoader]
private void load()
{
SetContents(() => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground), _ => null)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Width = 0.5f,
});
}
}
}

View File

@ -28,7 +28,9 @@ namespace osu.Game.Rulesets.Mania.Tests
{
typeof(Column),
typeof(ColumnBackground),
typeof(ColumnHitObjectArea)
typeof(ColumnHitObjectArea),
typeof(DefaultKeyArea),
typeof(DefaultHitTarget)
};
[Cached(typeof(IReadOnlyList<Mod>))]

View File

@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Mania.Tests
[Cached(typeof(IReadOnlyList<Mod>))]
private IReadOnlyList<Mod> mods { get; set; } = Array.Empty<Mod>();
private readonly List<ManiaStage> stages = new List<ManiaStage>();
private readonly List<Stage> stages = new List<Stage>();
private FillFlowContainer<ScrollingTestContainer> fill;
@ -81,9 +81,9 @@ namespace osu.Game.Rulesets.Mania.Tests
AddAssert("check bar anchors", () => barsInStageAreAnchored(stages[1], Anchor.TopCentre));
}
private bool notesInStageAreAnchored(ManiaStage stage, Anchor anchor) => stage.Columns.SelectMany(c => c.AllHitObjects).All(o => o.Anchor == anchor);
private bool notesInStageAreAnchored(Stage stage, Anchor anchor) => stage.Columns.SelectMany(c => c.AllHitObjects).All(o => o.Anchor == anchor);
private bool barsInStageAreAnchored(ManiaStage stage, Anchor anchor) => stage.AllHitObjects.Where(obj => obj is DrawableBarLine).All(o => o.Anchor == anchor);
private bool barsInStageAreAnchored(Stage stage, Anchor anchor) => stage.AllHitObjects.Where(obj => obj is DrawableBarLine).All(o => o.Anchor == anchor);
private void createNote()
{
@ -133,7 +133,7 @@ namespace osu.Game.Rulesets.Mania.Tests
{
var specialAction = ManiaAction.Special1;
var stage = new ManiaStage(0, new StageDefinition { Columns = 2 }, ref action, ref specialAction);
var stage = new Stage(0, new StageDefinition { Columns = 2 }, ref action, ref specialAction);
stages.Add(stage);
return new ScrollingTestContainer(direction)

View File

@ -45,6 +45,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
BorderThickness = 1,
BorderColour = colours.Yellow,
Child = new Box

View File

@ -39,6 +39,8 @@ namespace osu.Game.Rulesets.Mania
HoldNoteHead,
HoldNoteTail,
HoldNoteBody,
HitExplosion
HitExplosion,
StageBackground,
StageForeground,
}
}

View File

@ -0,0 +1,61 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Mania.Skinning
{
public class LegacyStageBackground : LegacyManiaElement
{
private Drawable leftSprite;
private Drawable rightSprite;
public LegacyStageBackground()
{
RelativeSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
{
string leftImage = GetManiaSkinConfig<string>(skin, LegacyManiaSkinConfigurationLookups.LeftStageImage)?.Value
?? "mania-stage-left";
string rightImage = GetManiaSkinConfig<string>(skin, LegacyManiaSkinConfigurationLookups.RightStageImage)?.Value
?? "mania-stage-right";
InternalChildren = new[]
{
leftSprite = new Sprite
{
Anchor = Anchor.TopLeft,
Origin = Anchor.TopRight,
X = 0.05f,
Texture = skin.GetTexture(leftImage),
},
rightSprite = new Sprite
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopLeft,
X = -0.05f,
Texture = skin.GetTexture(rightImage)
}
};
}
protected override void Update()
{
base.Update();
if (leftSprite?.Height > 0)
leftSprite.Scale = new Vector2(DrawHeight / leftSprite.Height);
if (rightSprite?.Height > 0)
rightSprite.Scale = new Vector2(DrawHeight / rightSprite.Height);
}
}
}

View File

@ -0,0 +1,56 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Mania.Skinning
{
public class LegacyStageForeground : LegacyManiaElement
{
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
private Drawable sprite;
public LegacyStageForeground()
{
RelativeSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load(ISkinSource skin, IScrollingInfo scrollingInfo)
{
string bottomImage = GetManiaSkinConfig<string>(skin, LegacyManiaSkinConfigurationLookups.BottomStageImage)?.Value
?? "mania-stage-bottom";
sprite = skin.GetAnimation(bottomImage, true, true)?.With(d =>
{
if (d == null)
return;
d.Scale = new Vector2(1.6f);
});
if (sprite != null)
InternalChild = sprite;
direction.BindTo(scrollingInfo.Direction);
direction.BindValueChanged(onDirectionChanged, true);
}
private void onDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)
{
if (sprite == null)
return;
if (direction.NewValue == ScrollingDirection.Up)
sprite.Anchor = sprite.Origin = Anchor.TopCentre;
else
sprite.Anchor = sprite.Origin = Anchor.BottomCentre;
}
}
}

View File

@ -81,6 +81,12 @@ namespace osu.Game.Rulesets.Mania.Skinning
case ManiaSkinComponents.HitExplosion:
return new LegacyHitExplosion();
case ManiaSkinComponents.StageBackground:
return new LegacyStageBackground();
case ManiaSkinComponents.StageForeground:
return new LegacyStageForeground();
}
break;

View File

@ -42,6 +42,7 @@ namespace osu.Game.Rulesets.Mania.UI
Index = index;
RelativeSizeAxes = Axes.Y;
Width = COLUMN_WIDTH;
Drawable background = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground, Index), _ => new DefaultColumnBackground())
{
@ -138,6 +139,6 @@ namespace osu.Game.Rulesets.Mania.UI
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
// This probably shouldn't exist as is, but the columns in the stage are separated by a 1px border
=> DrawRectangle.Inflate(new Vector2(ManiaStage.COLUMN_SPACING / 2, 0)).Contains(ToLocalSpace(screenSpacePos));
=> DrawRectangle.Inflate(new Vector2(Stage.COLUMN_SPACING / 2, 0)).Contains(ToLocalSpace(screenSpacePos));
}
}

View File

@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Mania.UI.Components
InternalChild = directionContainer = new Container
{
RelativeSizeAxes = Axes.X,
Height = ManiaStage.HIT_TARGET_POSITION,
Height = Stage.HIT_TARGET_POSITION,
Children = new[]
{
gradient = new Box
@ -53,9 +53,8 @@ namespace osu.Game.Rulesets.Mania.UI.Components
keyIcon = new Container
{
Name = "Key icon",
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(key_icon_size),
Origin = Anchor.Centre,
Masking = true,
CornerRadius = key_icon_corner_radius,
BorderThickness = 2,
@ -88,11 +87,15 @@ namespace osu.Game.Rulesets.Mania.UI.Components
{
if (direction.NewValue == ScrollingDirection.Up)
{
keyIcon.Anchor = Anchor.BottomCentre;
keyIcon.Y = -20;
directionContainer.Anchor = directionContainer.Origin = Anchor.TopLeft;
gradient.Colour = ColourInfo.GradientVertical(Color4.Black, Color4.Black.Opacity(0));
}
else
{
keyIcon.Anchor = Anchor.TopCentre;
keyIcon.Y = 20;
directionContainer.Anchor = directionContainer.Origin = Anchor.BottomLeft;
gradient.Colour = ColourInfo.GradientVertical(Color4.Black.Opacity(0), Color4.Black);
}

View File

@ -0,0 +1,30 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.UI.Components
{
public class DefaultStageBackground : CompositeDrawable
{
public DefaultStageBackground()
{
RelativeSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChild = new Box
{
Name = "Background",
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black
};
}
}
}

View File

@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Mania.UI.Components
{
float hitPosition = CurrentSkin.GetConfig<ManiaSkinConfigurationLookup, float>(
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.HitPosition))?.Value
?? ManiaStage.HIT_TARGET_POSITION;
?? Stage.HIT_TARGET_POSITION;
Padding = Direction.Value == ScrollingDirection.Up
? new MarginPadding { Top = hitPosition }

View File

@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Mania.UI
{
public class ManiaPlayfield : ScrollingPlayfield
{
private readonly List<ManiaStage> stages = new List<ManiaStage>();
private readonly List<Stage> stages = new List<Stage>();
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => stages.Any(s => s.ReceivePositionalInputAt(screenSpacePos));
@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Mania.UI
for (int i = 0; i < stageDefinitions.Count; i++)
{
var newStage = new ManiaStage(firstColumnIndex, stageDefinitions[i], ref normalColumnAction, ref specialColumnAction);
var newStage = new Stage(firstColumnIndex, stageDefinitions[i], ref normalColumnAction, ref specialColumnAction);
playfieldGrid.Content[0][i] = newStage;
@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Mania.UI
/// </summary>
public int TotalColumns => stages.Sum(s => s.Columns.Count);
private ManiaStage getStageByColumn(int column)
private Stage getStageByColumn(int column)
{
int sum = 0;

View File

@ -6,7 +6,6 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
@ -25,11 +24,11 @@ namespace osu.Game.Rulesets.Mania.UI
/// <summary>
/// A collection of <see cref="Column"/>s.
/// </summary>
public class ManiaStage : ScrollingPlayfield
public class Stage : ScrollingPlayfield
{
public const float COLUMN_SPACING = 1;
public const float HIT_TARGET_POSITION = 50;
public const float HIT_TARGET_POSITION = 110;
public IReadOnlyList<Column> Columns => columnFlow.Children;
private readonly FillFlowContainer<Column> columnFlow;
@ -51,7 +50,7 @@ namespace osu.Game.Rulesets.Mania.UI
private readonly int firstColumnIndex;
public ManiaStage(int firstColumnIndex, StageDefinition definition, ref ManiaAction normalColumnStartAction, ref ManiaAction specialColumnStartAction)
public Stage(int firstColumnIndex, StageDefinition definition, ref ManiaAction normalColumnStartAction, ref ManiaAction specialColumnStartAction)
{
this.firstColumnIndex = firstColumnIndex;
@ -72,11 +71,9 @@ namespace osu.Game.Rulesets.Mania.UI
AutoSizeAxes = Axes.X,
Children = new Drawable[]
{
new Box
new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground), _ => new DefaultStageBackground())
{
Name = "Background",
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black
RelativeSizeAxes = Axes.Both
},
columnFlow = new FillFlowContainer<Column>
{
@ -103,6 +100,10 @@ namespace osu.Game.Rulesets.Mania.UI
RelativeSizeAxes = Axes.Y,
}
},
new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground), _ => null)
{
RelativeSizeAxes = Axes.Both
},
judgements = new JudgementContainer<DrawableManiaJudgement>
{
Anchor = Anchor.TopCentre,

View File

@ -0,0 +1,412 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Screens;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Replays;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
{
public class TestSceneOutOfOrderHits : RateAdjustedBeatmapTestScene
{
private const double early_miss_window = 1000; // time after -1000 to -500 is considered a miss
private const double late_miss_window = 500; // time after +500 is considered a miss
/// <summary>
/// Tests clicking a future circle before the first circle's start time, while the first circle HAS NOT been judged.
/// </summary>
[Test]
public void TestClickSecondCircleBeforeFirstCircleTime()
{
const double time_first_circle = 1500;
const double time_second_circle = 1600;
Vector2 positionFirstCircle = Vector2.Zero;
Vector2 positionSecondCircle = new Vector2(80);
var hitObjects = new List<OsuHitObject>
{
new TestHitCircle
{
StartTime = time_first_circle,
Position = positionFirstCircle
},
new TestHitCircle
{
StartTime = time_second_circle,
Position = positionSecondCircle
}
};
performTest(hitObjects, new List<ReplayFrame>
{
new OsuReplayFrame { Time = time_first_circle - 100, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } }
});
addJudgementAssert(hitObjects[0], HitResult.Miss);
addJudgementAssert(hitObjects[1], HitResult.Miss);
addJudgementOffsetAssert(hitObjects[0], late_miss_window);
}
/// <summary>
/// Tests clicking a future circle at the first circle's start time, while the first circle HAS NOT been judged.
/// </summary>
[Test]
public void TestClickSecondCircleAtFirstCircleTime()
{
const double time_first_circle = 1500;
const double time_second_circle = 1600;
Vector2 positionFirstCircle = Vector2.Zero;
Vector2 positionSecondCircle = new Vector2(80);
var hitObjects = new List<OsuHitObject>
{
new TestHitCircle
{
StartTime = time_first_circle,
Position = positionFirstCircle
},
new TestHitCircle
{
StartTime = time_second_circle,
Position = positionSecondCircle
}
};
performTest(hitObjects, new List<ReplayFrame>
{
new OsuReplayFrame { Time = time_first_circle, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } }
});
addJudgementAssert(hitObjects[0], HitResult.Miss);
addJudgementAssert(hitObjects[1], HitResult.Great);
addJudgementOffsetAssert(hitObjects[0], 0);
}
/// <summary>
/// Tests clicking a future circle after the first circle's start time, while the first circle HAS NOT been judged.
/// </summary>
[Test]
public void TestClickSecondCircleAfterFirstCircleTime()
{
const double time_first_circle = 1500;
const double time_second_circle = 1600;
Vector2 positionFirstCircle = Vector2.Zero;
Vector2 positionSecondCircle = new Vector2(80);
var hitObjects = new List<OsuHitObject>
{
new TestHitCircle
{
StartTime = time_first_circle,
Position = positionFirstCircle
},
new TestHitCircle
{
StartTime = time_second_circle,
Position = positionSecondCircle
}
};
performTest(hitObjects, new List<ReplayFrame>
{
new OsuReplayFrame { Time = time_first_circle + 100, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } }
});
addJudgementAssert(hitObjects[0], HitResult.Miss);
addJudgementAssert(hitObjects[1], HitResult.Great);
addJudgementOffsetAssert(hitObjects[0], 100);
}
/// <summary>
/// Tests clicking a future circle before the first circle's start time, while the first circle HAS been judged.
/// </summary>
[Test]
public void TestClickSecondCircleBeforeFirstCircleTimeWithFirstCircleJudged()
{
const double time_first_circle = 1500;
const double time_second_circle = 1600;
Vector2 positionFirstCircle = Vector2.Zero;
Vector2 positionSecondCircle = new Vector2(80);
var hitObjects = new List<OsuHitObject>
{
new TestHitCircle
{
StartTime = time_first_circle,
Position = positionFirstCircle
},
new TestHitCircle
{
StartTime = time_second_circle,
Position = positionSecondCircle
}
};
performTest(hitObjects, new List<ReplayFrame>
{
new OsuReplayFrame { Time = time_first_circle - 200, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } },
new OsuReplayFrame { Time = time_first_circle - 100, Position = positionSecondCircle, Actions = { OsuAction.RightButton } }
});
addJudgementAssert(hitObjects[0], HitResult.Great);
addJudgementAssert(hitObjects[1], HitResult.Great);
addJudgementOffsetAssert(hitObjects[0], -200); // time_first_circle - 200
addJudgementOffsetAssert(hitObjects[0], -200); // time_second_circle - first_circle_time - 100
}
/// <summary>
/// Tests clicking a future circle after a slider's start time, but hitting all slider ticks.
/// </summary>
[Test]
public void TestMissSliderHeadAndHitAllSliderTicks()
{
const double time_slider = 1500;
const double time_circle = 1510;
Vector2 positionCircle = Vector2.Zero;
Vector2 positionSlider = new Vector2(80);
var hitObjects = new List<OsuHitObject>
{
new TestHitCircle
{
StartTime = time_circle,
Position = positionCircle
},
new TestSlider
{
StartTime = time_slider,
Position = positionSlider,
Path = new SliderPath(PathType.Linear, new[]
{
Vector2.Zero,
new Vector2(25, 0),
})
}
};
performTest(hitObjects, new List<ReplayFrame>
{
new OsuReplayFrame { Time = time_slider, Position = positionCircle, Actions = { OsuAction.LeftButton } },
new OsuReplayFrame { Time = time_slider + 10, Position = positionSlider, Actions = { OsuAction.RightButton } }
});
addJudgementAssert(hitObjects[0], HitResult.Great);
addJudgementAssert(hitObjects[1], HitResult.Great);
addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.Miss);
addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.Great);
}
/// <summary>
/// Tests clicking hitting future slider ticks before a circle.
/// </summary>
[Test]
public void TestHitSliderTicksBeforeCircle()
{
const double time_slider = 1500;
const double time_circle = 1510;
Vector2 positionCircle = Vector2.Zero;
Vector2 positionSlider = new Vector2(80);
var hitObjects = new List<OsuHitObject>
{
new TestHitCircle
{
StartTime = time_circle,
Position = positionCircle
},
new TestSlider
{
StartTime = time_slider,
Position = positionSlider,
Path = new SliderPath(PathType.Linear, new[]
{
Vector2.Zero,
new Vector2(25, 0),
})
}
};
performTest(hitObjects, new List<ReplayFrame>
{
new OsuReplayFrame { Time = time_slider, Position = positionSlider, Actions = { OsuAction.LeftButton } },
new OsuReplayFrame { Time = time_circle + late_miss_window - 100, Position = positionCircle, Actions = { OsuAction.RightButton } },
new OsuReplayFrame { Time = time_circle + late_miss_window - 90, Position = positionSlider, Actions = { OsuAction.LeftButton } },
});
addJudgementAssert(hitObjects[0], HitResult.Great);
addJudgementAssert(hitObjects[1], HitResult.Great);
addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.Great);
addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.Great);
}
/// <summary>
/// Tests clicking a future circle before a spinner.
/// </summary>
[Test]
public void TestHitCircleBeforeSpinner()
{
const double time_spinner = 1500;
const double time_circle = 1800;
Vector2 positionCircle = Vector2.Zero;
var hitObjects = new List<OsuHitObject>
{
new TestSpinner
{
StartTime = time_spinner,
Position = new Vector2(256, 192),
EndTime = time_spinner + 1000,
},
new TestHitCircle
{
StartTime = time_circle,
Position = positionCircle
},
};
performTest(hitObjects, new List<ReplayFrame>
{
new OsuReplayFrame { Time = time_spinner - 100, Position = positionCircle, Actions = { OsuAction.LeftButton } },
new OsuReplayFrame { Time = time_spinner + 10, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } },
new OsuReplayFrame { Time = time_spinner + 20, Position = new Vector2(256, 172), Actions = { OsuAction.RightButton } },
new OsuReplayFrame { Time = time_spinner + 30, Position = new Vector2(276, 192), Actions = { OsuAction.RightButton } },
new OsuReplayFrame { Time = time_spinner + 40, Position = new Vector2(256, 212), Actions = { OsuAction.RightButton } },
new OsuReplayFrame { Time = time_spinner + 50, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } },
});
addJudgementAssert(hitObjects[0], HitResult.Great);
addJudgementAssert(hitObjects[1], HitResult.Great);
}
private void addJudgementAssert(OsuHitObject hitObject, HitResult result)
{
AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}",
() => judgementResults.Single(r => r.HitObject == hitObject).Type == result);
}
private void addJudgementAssert(string name, Func<OsuHitObject> hitObject, HitResult result)
{
AddAssert($"{name} judgement is {result}",
() => judgementResults.Single(r => r.HitObject == hitObject()).Type == result);
}
private void addJudgementOffsetAssert(OsuHitObject hitObject, double offset)
{
AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judged at {offset}",
() => Precision.AlmostEquals(judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, offset, 100));
}
private ScoreAccessibleReplayPlayer currentPlayer;
private List<JudgementResult> judgementResults;
private bool allJudgedFired;
private void performTest(List<OsuHitObject> hitObjects, List<ReplayFrame> frames)
{
AddStep("load player", () =>
{
Beatmap.Value = CreateWorkingBeatmap(new Beatmap<OsuHitObject>
{
HitObjects = hitObjects,
BeatmapInfo =
{
BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 3 },
Ruleset = new OsuRuleset().RulesetInfo
},
});
Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
p.OnLoadComplete += _ =>
{
p.ScoreProcessor.NewJudgement += result =>
{
if (currentPlayer == p) judgementResults.Add(result);
};
p.ScoreProcessor.AllJudged += () =>
{
if (currentPlayer == p) allJudgedFired = true;
};
};
LoadScreen(currentPlayer = p);
allJudgedFired = false;
judgementResults = new List<JudgementResult>();
});
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
AddUntilStep("Wait for all judged", () => allJudgedFired);
}
private class TestHitCircle : HitCircle
{
protected override HitWindows CreateHitWindows() => new TestHitWindows();
}
private class TestSlider : Slider
{
public TestSlider()
{
DefaultsApplied += () =>
{
HeadCircle.HitWindows = new TestHitWindows();
TailCircle.HitWindows = new TestHitWindows();
};
}
}
private class TestSpinner : Spinner
{
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
SpinsRequired = 1;
}
}
private class TestHitWindows : HitWindows
{
private static readonly DifficultyRange[] ranges =
{
new DifficultyRange(HitResult.Great, 500, 500, 500),
new DifficultyRange(HitResult.Miss, early_miss_window, early_miss_window, early_miss_window),
};
public override bool IsHitResultAllowed(HitResult result) => result == HitResult.Great || result == HitResult.Miss;
protected override DifficultyRange[] GetRanges() => ranges;
}
private class ScoreAccessibleReplayPlayer : ReplayPlayer
{
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
protected override bool PauseOnFocusLost => false;
public ScoreAccessibleReplayPlayer(Score score)
: base(score, false, false)
{
}
}
}
}

View File

@ -15,6 +15,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose;
using osuTK;
using osuTK.Input;
@ -34,6 +35,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
[Resolved(CanBeNull = true)]
private IPlacementHandler placementHandler { get; set; }
[Resolved(CanBeNull = true)]
private EditorBeatmap editorBeatmap { get; set; }
public SliderSelectionBlueprint(DrawableSlider slider)
: base(slider)
{
@ -162,7 +166,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private void updatePath()
{
HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
UpdateHitObject();
editorBeatmap?.UpdateHitObject(HitObject);
}
public override MenuItem[] ContextMenuItems => new MenuItem[]

View File

@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
var result = HitObject.HitWindows.ResultFor(timeOffset);
if (result == HitResult.None)
if (result == HitResult.None || CheckHittable?.Invoke(this, Time.Current) == false)
{
Shake(Math.Abs(timeOffset) - HitObject.HitWindows.WindowFor(HitResult.Miss));
return;

View File

@ -1,11 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
@ -16,6 +18,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
// Must be set to update IsHovered as it's used in relax mdo to detect osu hit objects.
public override bool HandlePositionalInput => true;
/// <summary>
/// Whether this <see cref="DrawableOsuHitObject"/> can be hit.
/// If non-null, judgements will be ignored (resulting in a shake) whilst the function returns false.
/// </summary>
public Func<DrawableHitObject, double, bool> CheckHittable;
protected DrawableOsuHitObject(OsuHitObject hitObject)
: base(hitObject)
{
@ -54,6 +62,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}
}
/// <summary>
/// Causes this <see cref="DrawableOsuHitObject"/> to get missed, disregarding all conditions in implementations of <see cref="DrawableHitObject.CheckForResult"/>.
/// </summary>
public void MissForcefully() => ApplyResult(r => r.Type = HitResult.Miss);
protected override JudgementResult CreateResult(Judgement judgement) => new OsuJudgementResult(HitObject, judgement);
}
}

View File

@ -124,7 +124,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
case SliderTailCircle tail:
return new DrawableSliderTail(slider, tail);
case HitCircle head:
case SliderHeadCircle head:
return new DrawableSliderHead(slider, head) { OnShake = Shake };
case SliderTick tick:

View File

@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private readonly Slider slider;
public DrawableSliderHead(Slider slider, HitCircle h)
public DrawableSliderHead(Slider slider, SliderHeadCircle h)
: base(h)
{
this.slider = slider;

View File

@ -31,7 +31,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
Blending = BlendingParameters.Additive;
Origin = Anchor.Centre;
InternalChild = scaleContainer = new ReverseArrowPiece();

View File

@ -8,11 +8,16 @@ using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Containers;
using osu.Game.Skinning;
using osu.Framework.Allocation;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
public class ReverseArrowPiece : BeatSyncedContainer
{
[Resolved]
private DrawableHitObject drawableRepeat { get; set; }
public ReverseArrowPiece()
{
Divisor = 2;
@ -21,13 +26,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
Blending = BlendingParameters.Additive;
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.ReverseArrow), _ => new SpriteIcon
{
RelativeSizeAxes = Axes.Both,
Blending = BlendingParameters.Additive,
Icon = FontAwesome.Solid.ChevronRight,
Size = new Vector2(0.35f)
})
@ -37,7 +41,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
};
}
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) =>
Child.ScaleTo(1.3f).ScaleTo(1f, timingPoint.BeatLength, Easing.Out);
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes)
{
if (!drawableRepeat.IsHit)
Child.ScaleTo(1.3f).ScaleTo(1f, timingPoint.BeatLength, Easing.Out);
}
}
}

View File

@ -40,7 +40,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
this.drawableSlider = drawableSlider;
this.slider = slider;
Blending = BlendingParameters.Additive;
Origin = Anchor.Centre;
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
@ -241,6 +240,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
Scale = new Vector2(radius / OsuHitObject.OBJECT_RADIUS),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Blending = BlendingParameters.Additive,
BorderThickness = 10,
BorderColour = Color4.White,
Alpha = 1,

View File

@ -155,7 +155,7 @@ namespace osu.Game.Rulesets.Osu.Objects
break;
case SliderEventType.Head:
AddNested(HeadCircle = new SliderCircle
AddNested(HeadCircle = new SliderHeadCircle
{
StartTime = e.Time,
Position = Position,

View File

@ -0,0 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Game.Rulesets.Osu.Objects
{
public class SliderHeadCircle : HitCircle
{
}
}

View File

@ -0,0 +1,127 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.UI
{
/// <summary>
/// Ensures that <see cref="HitObject"/>s are hit in-order.
/// If a <see cref="HitObject"/> is hit out of order:
/// <list type="number">
/// <item><description>The hit is blocked if it occurred earlier than the previous <see cref="HitObject"/>'s start time.</description></item>
/// <item><description>The hit causes all previous <see cref="HitObject"/>s to missed otherwise.</description></item>
/// </list>
/// </summary>
public class OrderedHitPolicy
{
private readonly HitObjectContainer hitObjectContainer;
public OrderedHitPolicy(HitObjectContainer hitObjectContainer)
{
this.hitObjectContainer = hitObjectContainer;
}
/// <summary>
/// Determines whether a <see cref="DrawableHitObject"/> can be hit at a point in time.
/// </summary>
/// <param name="hitObject">The <see cref="DrawableHitObject"/> to check.</param>
/// <param name="time">The time to check.</param>
/// <returns>Whether <paramref name="hitObject"/> can be hit at the given <paramref name="time"/>.</returns>
public bool IsHittable(DrawableHitObject hitObject, double time)
{
DrawableHitObject blockingObject = null;
// Find the last hitobject which blocks future hits.
foreach (var obj in hitObjectContainer.AliveObjects)
{
if (obj == hitObject)
break;
if (drawableCanBlockFutureHits(obj))
blockingObject = obj;
}
// If there is no previous hitobject, allow the hit.
if (blockingObject == null)
return true;
// A hit is allowed if:
// 1. The last blocking hitobject has been judged.
// 2. The current time is after the last hitobject's start time.
// Hits at exactly the same time as the blocking hitobject are allowed for maps that contain simultaneous hitobjects (e.g. /b/372245).
if (blockingObject.Judged || time >= blockingObject.HitObject.StartTime)
return true;
return false;
}
/// <summary>
/// Handles a <see cref="HitObject"/> being hit to potentially miss all earlier <see cref="HitObject"/>s.
/// </summary>
/// <param name="hitObject">The <see cref="HitObject"/> that was hit.</param>
public void HandleHit(HitObject hitObject)
{
// Hitobjects which themselves don't block future hitobjects don't cause misses (e.g. slider ticks, spinners).
if (!hitObjectCanBlockFutureHits(hitObject))
return;
double maximumTime = hitObject.StartTime;
// Iterate through and apply miss results to all top-level and nested hitobjects which block future hits.
foreach (var obj in hitObjectContainer.AliveObjects)
{
if (obj.Judged || obj.HitObject.StartTime >= maximumTime)
continue;
if (hitObjectCanBlockFutureHits(obj.HitObject))
applyMiss(obj);
foreach (var nested in obj.NestedHitObjects)
{
if (nested.Judged || nested.HitObject.StartTime >= maximumTime)
continue;
if (hitObjectCanBlockFutureHits(nested.HitObject))
applyMiss(nested);
}
}
static void applyMiss(DrawableHitObject obj) => ((DrawableOsuHitObject)obj).MissForcefully();
}
/// <summary>
/// Whether a <see cref="DrawableHitObject"/> blocks hits on future <see cref="DrawableHitObject"/>s until its start time is reached.
/// </summary>
/// <remarks>
/// This will ONLY match on top-most <see cref="DrawableHitObject"/>s.
/// </remarks>
/// <param name="hitObject">The <see cref="DrawableHitObject"/> to test.</param>
private static bool drawableCanBlockFutureHits(DrawableHitObject hitObject)
{
// Special considerations for slider tails aren't required since only top-most drawable hitobjects are being iterated over.
return hitObject is DrawableHitCircle || hitObject is DrawableSlider;
}
/// <summary>
/// Whether a <see cref="HitObject"/> blocks hits on future <see cref="HitObject"/>s until its start time is reached.
/// </summary>
/// <remarks>
/// This is more rigorous and may not match on top-most <see cref="HitObject"/>s as <see cref="drawableCanBlockFutureHits"/> does.
/// </remarks>
/// <param name="hitObject">The <see cref="HitObject"/> to test.</param>
private static bool hitObjectCanBlockFutureHits(HitObject hitObject)
{
// Unlike the above we will receive slider tails, but they do not block future hits.
if (hitObject is SliderTailCircle)
return false;
// All other hitcircles continue to block future hits.
return hitObject is HitCircle;
}
}
}

View File

@ -20,6 +20,7 @@ namespace osu.Game.Rulesets.Osu.UI
private readonly ApproachCircleProxyContainer approachCircles;
private readonly JudgementContainer<DrawableOsuJudgement> judgementLayer;
private readonly FollowPointRenderer followPoints;
private readonly OrderedHitPolicy hitPolicy;
public static readonly Vector2 BASE_SIZE = new Vector2(512, 384);
@ -51,6 +52,8 @@ namespace osu.Game.Rulesets.Osu.UI
Depth = -1,
},
};
hitPolicy = new OrderedHitPolicy(HitObjectContainer);
}
public override void Add(DrawableHitObject h)
@ -64,7 +67,10 @@ namespace osu.Game.Rulesets.Osu.UI
base.Add(h);
followPoints.AddFollowPoints((DrawableOsuHitObject)h);
DrawableOsuHitObject osuHitObject = (DrawableOsuHitObject)h;
osuHitObject.CheckHittable = hitPolicy.IsHittable;
followPoints.AddFollowPoints(osuHitObject);
}
public override bool Remove(DrawableHitObject h)
@ -79,6 +85,9 @@ namespace osu.Game.Rulesets.Osu.UI
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)
{
// Hitobjects that block future hits should miss previous hitobjects if they're hit out-of-order.
hitPolicy.HandleHit(result.HitObject);
if (!judgedObject.DisplayResult || !DisplayJudgements.Value)
return;

View File

@ -0,0 +1,5 @@
[General]
Name: an old skin
Author: an old guy
// no version specified means v1

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@ -18,9 +18,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning
/// </summary>
internal class LegacyInputDrum : Container
{
private LegacyHalfDrum left;
private LegacyHalfDrum right;
public LegacyInputDrum()
{
AutoSizeAxes = Axes.Both;
Size = new Vector2(180, 200);
}
[BackgroundDependencyLoader]
@ -32,25 +35,47 @@ namespace osu.Game.Rulesets.Taiko.Skinning
{
Texture = skin.GetTexture("taiko-bar-left")
},
new LegacyHalfDrum(false)
left = new LegacyHalfDrum(false)
{
Name = "Left Half",
RelativeSizeAxes = Axes.Both,
Width = 0.5f,
RimAction = TaikoAction.LeftRim,
CentreAction = TaikoAction.LeftCentre
},
new LegacyHalfDrum(true)
right = new LegacyHalfDrum(true)
{
Name = "Right Half",
Anchor = Anchor.TopRight,
RelativeSizeAxes = Axes.Both,
Width = 0.5f,
Origin = Anchor.TopRight,
Scale = new Vector2(-1, 1),
RimAction = TaikoAction.RightRim,
CentreAction = TaikoAction.RightCentre
}
};
// this will be used in the future for stable skin alignment. keeping here for reference.
const float taiko_bar_y = 0;
// stable things
const float ratio = 1.6f;
// because the right half is flipped, we need to position using width - position to get the true "topleft" origin position
float negativeScaleAdjust = Width / ratio;
if (skin.GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version)?.Value >= 2.1m)
{
left.Centre.Position = new Vector2(0, taiko_bar_y) * ratio;
right.Centre.Position = new Vector2(negativeScaleAdjust - 56, taiko_bar_y) * ratio;
left.Rim.Position = new Vector2(0, taiko_bar_y) * ratio;
right.Rim.Position = new Vector2(negativeScaleAdjust - 56, taiko_bar_y) * ratio;
}
else
{
left.Centre.Position = new Vector2(18, taiko_bar_y + 31) * ratio;
right.Centre.Position = new Vector2(negativeScaleAdjust - 54, taiko_bar_y + 31) * ratio;
left.Rim.Position = new Vector2(8, taiko_bar_y + 23) * ratio;
right.Rim.Position = new Vector2(negativeScaleAdjust - 53, taiko_bar_y + 23) * ratio;
}
}
/// <summary>
@ -68,8 +93,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning
/// </summary>
public TaikoAction CentreAction;
private readonly Sprite rimHit;
private readonly Sprite centreHit;
public readonly Sprite Rim;
public readonly Sprite Centre;
[Resolved]
private DrumSampleMapping sampleMappings { get; set; }
@ -80,18 +105,16 @@ namespace osu.Game.Rulesets.Taiko.Skinning
Children = new Drawable[]
{
rimHit = new Sprite
Rim = new Sprite
{
Anchor = flipped ? Anchor.CentreRight : Anchor.CentreLeft,
Origin = flipped ? Anchor.CentreLeft : Anchor.CentreRight,
Scale = new Vector2(-1, 1),
Origin = flipped ? Anchor.TopLeft : Anchor.TopRight,
Alpha = 0,
},
centreHit = new Sprite
Centre = new Sprite
{
Anchor = flipped ? Anchor.CentreRight : Anchor.CentreLeft,
Origin = flipped ? Anchor.CentreRight : Anchor.CentreLeft,
Alpha = 0,
Origin = flipped ? Anchor.TopRight : Anchor.TopLeft,
}
};
}
@ -99,8 +122,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
{
rimHit.Texture = skin.GetTexture(@"taiko-drum-outer");
centreHit.Texture = skin.GetTexture(@"taiko-drum-inner");
Rim.Texture = skin.GetTexture(@"taiko-drum-outer");
Centre.Texture = skin.GetTexture(@"taiko-drum-inner");
}
public bool OnPressed(TaikoAction action)
@ -110,12 +133,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning
if (action == CentreAction)
{
target = centreHit;
target = Centre;
drumSample.Centre?.Play();
}
else if (action == RimAction)
{
target = rimHit;
target = Rim;
drumSample.Rim?.Play();
}

View File

@ -1,18 +1,21 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using Microsoft.EntityFrameworkCore.Internal;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit;
using osu.Game.Tests.Visual;
namespace osu.Game.Tests.Beatmaps
{
[TestFixture]
public class EditorBeatmapTest
[HeadlessTest]
public class TestSceneEditorBeatmap : EditorClockTestScene
{
/// <summary>
/// Tests that the addition event is correctly invoked after a hitobject is added.
@ -55,13 +58,19 @@ namespace osu.Game.Tests.Beatmaps
public void TestInitialHitObjectStartTimeChangeEvent()
{
var hitCircle = new HitCircle();
var editorBeatmap = new EditorBeatmap(new OsuBeatmap { HitObjects = { hitCircle } });
HitObject changedObject = null;
editorBeatmap.StartTimeChanged += h => changedObject = h;
hitCircle.StartTime = 1000;
Assert.That(changedObject, Is.EqualTo(hitCircle));
AddStep("add beatmap", () =>
{
EditorBeatmap editorBeatmap;
Child = editorBeatmap = new EditorBeatmap(new OsuBeatmap { HitObjects = { hitCircle } });
editorBeatmap.HitObjectUpdated += h => changedObject = h;
});
AddStep("change start time", () => hitCircle.StartTime = 1000);
AddAssert("received change event", () => changedObject == hitCircle);
}
/// <summary>
@ -71,18 +80,22 @@ namespace osu.Game.Tests.Beatmaps
[Test]
public void TestAddedHitObjectStartTimeChangeEvent()
{
var editorBeatmap = new EditorBeatmap(new OsuBeatmap());
EditorBeatmap editorBeatmap = null;
HitObject changedObject = null;
editorBeatmap.StartTimeChanged += h => changedObject = h;
AddStep("add beatmap", () =>
{
Child = editorBeatmap = new EditorBeatmap(new OsuBeatmap());
editorBeatmap.HitObjectUpdated += h => changedObject = h;
});
var hitCircle = new HitCircle();
editorBeatmap.Add(hitCircle);
Assert.That(changedObject, Is.Null);
AddStep("add object", () => editorBeatmap.Add(hitCircle));
AddAssert("event not received", () => changedObject == null);
hitCircle.StartTime = 1000;
Assert.That(changedObject, Is.EqualTo(hitCircle));
AddStep("change start time", () => hitCircle.StartTime = 1000);
AddAssert("event received", () => changedObject == hitCircle);
}
/// <summary>
@ -95,7 +108,7 @@ namespace osu.Game.Tests.Beatmaps
var editorBeatmap = new EditorBeatmap(new OsuBeatmap { HitObjects = { hitCircle } });
HitObject changedObject = null;
editorBeatmap.StartTimeChanged += h => changedObject = h;
editorBeatmap.HitObjectUpdated += h => changedObject = h;
editorBeatmap.Remove(hitCircle);
Assert.That(changedObject, Is.Null);
@ -150,5 +163,69 @@ namespace osu.Game.Tests.Beatmaps
Assert.That(editorBeatmap.HitObjects.Count(h => h == hitCircle), Is.EqualTo(1));
Assert.That(editorBeatmap.HitObjects.IndexOf(hitCircle), Is.EqualTo(1));
}
/// <summary>
/// Tests that multiple hitobjects are updated simultaneously.
/// </summary>
[Test]
public void TestMultipleHitObjectUpdate()
{
var updatedObjects = new List<HitObject>();
var allHitObjects = new List<HitObject>();
EditorBeatmap editorBeatmap = null;
AddStep("add beatmap", () =>
{
updatedObjects.Clear();
Child = editorBeatmap = new EditorBeatmap(new OsuBeatmap());
for (int i = 0; i < 10; i++)
{
var h = new HitCircle();
editorBeatmap.Add(h);
allHitObjects.Add(h);
}
});
AddStep("change all start times", () =>
{
editorBeatmap.HitObjectUpdated += h => updatedObjects.Add(h);
for (int i = 0; i < 10; i++)
allHitObjects[i].StartTime += 10;
});
// Distinct ensures that all hitobjects have been updated once, debounce is tested below.
AddAssert("all hitobjects updated", () => updatedObjects.Distinct().Count() == 10);
}
/// <summary>
/// Tests that hitobject updates are debounced when they happen too soon.
/// </summary>
[Test]
public void TestDebouncedUpdate()
{
var updatedObjects = new List<HitObject>();
EditorBeatmap editorBeatmap = null;
AddStep("add beatmap", () =>
{
updatedObjects.Clear();
Child = editorBeatmap = new EditorBeatmap(new OsuBeatmap());
editorBeatmap.Add(new HitCircle());
});
AddStep("change start time twice", () =>
{
editorBeatmap.HitObjectUpdated += h => updatedObjects.Add(h);
editorBeatmap.HitObjects[0].StartTime = 10;
editorBeatmap.HitObjects[0].StartTime = 20;
});
AddAssert("only updated once", () => updatedObjects.Count == 1);
}
}
}

View File

@ -0,0 +1,4 @@
[Mania]
Keys: 4
ColumnWidth: 10,10,10,10
WidthForNoteHeightScale: 0

View File

@ -99,5 +99,20 @@ namespace osu.Game.Tests.Skins
Assert.That(configs[0].CustomColours, Contains.Key("ColourBarline").And.ContainValue(new Color4(50, 50, 50, 50)));
}
}
[Test]
public void TestMinimumColumnWidthFallsBackWhenZeroIsProvided()
{
var decoder = new LegacyManiaSkinDecoder();
using (var resStream = TestResources.OpenResource("mania-skin-zero-minwidth.ini"))
using (var stream = new LineBufferedReader(resStream))
{
var configs = decoder.Decode(stream);
Assert.That(configs.Count, Is.EqualTo(1));
Assert.That(configs[0].MinimumColumnWidth, Is.EqualTo(16));
}
}
}
}

View File

@ -8,7 +8,6 @@ using System.Collections.Generic;
using osu.Framework.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Framework.Allocation;
using osu.Game.Graphics.UserInterface;
using osu.Framework.Graphics.Shapes;
using osuTK.Graphics;
@ -100,21 +99,21 @@ namespace osu.Game.Tests.Visual.UserInterface
private class TestNoBackgroundHeader : OverlayHeader
{
protected override ScreenTitle CreateTitle() => new TestTitle();
protected override OverlayTitle CreateTitle() => new TestTitle();
}
private class TestNoControlHeader : OverlayHeader
{
protected override Drawable CreateBackground() => new OverlayHeaderBackground(@"Headers/changelog");
protected override ScreenTitle CreateTitle() => new TestTitle();
protected override OverlayTitle CreateTitle() => new TestTitle();
}
private class TestStringTabControlHeader : TabControlOverlayHeader<string>
{
protected override Drawable CreateBackground() => new OverlayHeaderBackground(@"Headers/news");
protected override ScreenTitle CreateTitle() => new TestTitle();
protected override OverlayTitle CreateTitle() => new TestTitle();
protected override Drawable CreateTitleContent() => new OverlayRulesetSelector();
@ -129,7 +128,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
protected override Drawable CreateBackground() => new OverlayHeaderBackground(@"Headers/rankings");
protected override ScreenTitle CreateTitle() => new TestTitle();
protected override OverlayTitle CreateTitle() => new TestTitle();
}
private enum TestEnum
@ -141,7 +140,7 @@ namespace osu.Game.Tests.Visual.UserInterface
private class TestBreadcrumbControlHeader : BreadcrumbControlOverlayHeader
{
protected override ScreenTitle CreateTitle() => new TestTitle();
protected override OverlayTitle CreateTitle() => new TestTitle();
public TestBreadcrumbControlHeader()
{
@ -151,15 +150,13 @@ namespace osu.Game.Tests.Visual.UserInterface
}
}
private class TestTitle : ScreenTitle
private class TestTitle : OverlayTitle
{
public TestTitle()
{
Title = "title";
Section = "section";
IconTexture = "Icons/changelog";
}
protected override Drawable CreateIcon() => new ScreenTitleTextureIcon(@"Icons/changelog");
}
}
}

View File

@ -22,6 +22,8 @@ namespace osu.Game.Tournament.Screens.Ladder
protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) => false;
protected override void OnDrag(DragEvent e)
{
this.MoveTo(target += e.Delta, 1000, Easing.OutQuint);

View File

@ -111,7 +111,7 @@ namespace osu.Game.Beatmaps.Formats
writer.WriteLine(FormattableString.Invariant($"Source: {beatmap.Metadata.Source}"));
writer.WriteLine(FormattableString.Invariant($"Tags: {beatmap.Metadata.Tags}"));
writer.WriteLine(FormattableString.Invariant($"BeatmapID: {beatmap.BeatmapInfo.OnlineBeatmapID ?? 0}"));
writer.WriteLine(FormattableString.Invariant($"BeatmapSetID: {beatmap.BeatmapInfo.BeatmapSet.OnlineBeatmapSetID ?? -1}"));
writer.WriteLine(FormattableString.Invariant($"BeatmapSetID: {beatmap.BeatmapInfo.BeatmapSet?.OnlineBeatmapSetID ?? -1}"));
}
private void handleDifficulty(TextWriter writer)

View File

@ -113,13 +113,13 @@ namespace osu.Game.Graphics.UserInterface
private const float transition_length = 500;
private void fadeActive()
protected void FadeHovered()
{
Bar.FadeIn(transition_length, Easing.OutQuint);
Text.FadeColour(Color4.White, transition_length, Easing.OutQuint);
}
private void fadeInactive()
protected void FadeUnhovered()
{
Bar.FadeOut(transition_length, Easing.OutQuint);
Text.FadeColour(AccentColour, transition_length, Easing.OutQuint);
@ -128,14 +128,14 @@ namespace osu.Game.Graphics.UserInterface
protected override bool OnHover(HoverEvent e)
{
if (!Active.Value)
fadeActive();
FadeHovered();
return true;
}
protected override void OnHoverLost(HoverLostEvent e)
{
if (!Active.Value)
fadeInactive();
FadeUnhovered();
}
[BackgroundDependencyLoader]
@ -172,13 +172,19 @@ namespace osu.Game.Graphics.UserInterface
},
new HoverClickSounds()
};
Active.BindValueChanged(active => Text.Font = Text.Font.With(Typeface.Torus, weight: active.NewValue ? FontWeight.Bold : FontWeight.Medium), true);
}
protected override void OnActivated() => fadeActive();
protected override void OnActivated()
{
Text.Font = Text.Font.With(weight: FontWeight.Bold);
FadeHovered();
}
protected override void OnDeactivated() => fadeInactive();
protected override void OnDeactivated()
{
Text.Font = Text.Font.With(weight: FontWeight.Medium);
FadeUnhovered();
}
}
}
}

View File

@ -1,102 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics.Sprites;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Graphics.UserInterface
{
public abstract class ScreenTitle : CompositeDrawable, IHasAccentColour
{
public const float ICON_WIDTH = ICON_SIZE + spacing;
public const float ICON_SIZE = 25;
private const float spacing = 6;
private const int text_offset = 2;
private SpriteIcon iconSprite;
private readonly OsuSpriteText titleText, pageText;
protected IconUsage Icon
{
set
{
if (iconSprite == null)
throw new InvalidOperationException($"Cannot use {nameof(Icon)} with a custom {nameof(CreateIcon)} function.");
iconSprite.Icon = value;
}
}
protected string Title
{
set => titleText.Text = value;
}
protected string Section
{
set => pageText.Text = value;
}
public Color4 AccentColour
{
get => pageText.Colour;
set => pageText.Colour = value;
}
protected virtual Drawable CreateIcon() => iconSprite = new SpriteIcon
{
Size = new Vector2(ICON_SIZE),
};
protected ScreenTitle()
{
AutoSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(spacing, 0),
Direction = FillDirection.Horizontal,
Children = new[]
{
CreateIcon().With(t =>
{
t.Anchor = Anchor.Centre;
t.Origin = Anchor.Centre;
}),
titleText = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.GetFont(size: 20, weight: FontWeight.Bold),
Margin = new MarginPadding { Bottom = text_offset }
},
new Circle
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(4),
Colour = Color4.Gray,
},
pageText = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.GetFont(size: 20),
Margin = new MarginPadding { Bottom = text_offset }
}
}
},
};
}
}
}

View File

@ -1,40 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osuTK;
namespace osu.Game.Graphics.UserInterface
{
/// <summary>
/// A custom icon class for use with <see cref="ScreenTitle.CreateIcon()"/> based off a texture resource.
/// </summary>
public class ScreenTitleTextureIcon : CompositeDrawable
{
private readonly string textureName;
public ScreenTitleTextureIcon(string textureName)
{
this.textureName = textureName;
}
[BackgroundDependencyLoader]
private void load(TextureStore textures)
{
Size = new Vector2(ScreenTitle.ICON_SIZE);
InternalChild = new Sprite
{
RelativeSizeAxes = Axes.Both,
Texture = textures.Get(textureName),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
FillMode = FillMode.Fit
};
}
}
}

View File

@ -7,23 +7,31 @@ using osu.Game.Scoring;
namespace osu.Game.Online.Leaderboards
{
public class UpdateableRank : ModelBackedDrawable<ScoreRank>
public class UpdateableRank : ModelBackedDrawable<ScoreRank?>
{
public ScoreRank Rank
public ScoreRank? Rank
{
get => Model;
set => Model = value;
}
public UpdateableRank(ScoreRank rank)
public UpdateableRank(ScoreRank? rank)
{
Rank = rank;
}
protected override Drawable CreateDrawable(ScoreRank rank) => new DrawableRank(rank)
protected override Drawable CreateDrawable(ScoreRank? rank)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
};
if (rank.HasValue)
{
return new DrawableRank(rank.Value)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
};
}
return null;
}
}
}

View File

@ -1,24 +1,19 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.BeatmapListing
{
public class BeatmapListingHeader : OverlayHeader
{
protected override ScreenTitle CreateTitle() => new BeatmapListingTitle();
protected override OverlayTitle CreateTitle() => new BeatmapListingTitle();
private class BeatmapListingTitle : ScreenTitle
private class BeatmapListingTitle : OverlayTitle
{
public BeatmapListingTitle()
{
Title = @"beatmap";
Section = @"listing";
Title = "beatmap listing";
IconTexture = "Icons/changelog";
}
protected override Drawable CreateIcon() => new ScreenTitleTextureIcon(@"Icons/changelog");
}
}
}

View File

@ -3,7 +3,6 @@
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets;
namespace osu.Game.Overlays.BeatmapSet
@ -14,22 +13,20 @@ namespace osu.Game.Overlays.BeatmapSet
public BeatmapRulesetSelector RulesetSelector { get; private set; }
protected override ScreenTitle CreateTitle() => new BeatmapHeaderTitle();
protected override OverlayTitle CreateTitle() => new BeatmapHeaderTitle();
protected override Drawable CreateTitleContent() => RulesetSelector = new BeatmapRulesetSelector
{
Current = Ruleset
};
private class BeatmapHeaderTitle : ScreenTitle
private class BeatmapHeaderTitle : OverlayTitle
{
public BeatmapHeaderTitle()
{
Title = @"beatmap";
Section = @"info";
Title = "beatmap info";
IconTexture = "Icons/changelog";
}
protected override Drawable CreateIcon() => new ScreenTitleTextureIcon(@"Icons/changelog");
}
}
}

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.UserInterface;
@ -16,6 +17,13 @@ namespace osu.Game.Overlays
public OverlayHeaderBreadcrumbControl()
{
RelativeSizeAxes = Axes.X;
Height = 47;
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
AccentColour = colourProvider.Light2;
}
protected override TabItem<string> CreateTabItem(string value) => new ControlTabItem(value);
@ -27,10 +35,18 @@ namespace osu.Game.Overlays
public ControlTabItem(string value)
: base(value)
{
RelativeSizeAxes = Axes.Y;
Text.Font = Text.Font.With(size: 14);
Chevron.Y = 3;
Text.Anchor = Anchor.CentreLeft;
Text.Origin = Anchor.CentreLeft;
Chevron.Y = 1;
Bar.Height = 0;
}
// base OsuTabItem makes font bold on activation, we don't want that here
protected override void OnActivated() => FadeHovered();
protected override void OnDeactivated() => FadeUnhovered();
}
}
}

View File

@ -9,7 +9,6 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Overlays.Changelog
@ -50,8 +49,6 @@ namespace osu.Game.Overlays.Changelog
streamsBackground.Colour = colourProvider.Background5;
}
private ChangelogHeaderTitle title;
private void showBuild(ValueChangedEvent<APIChangelogBuild> e)
{
if (e.OldValue != null)
@ -63,14 +60,11 @@ namespace osu.Game.Overlays.Changelog
Current.Value = e.NewValue.ToString();
updateCurrentStream();
title.Version = e.NewValue.UpdateStream.DisplayName;
}
else
{
Current.Value = listing_string;
Streams.Current.Value = null;
title.Version = null;
}
}
@ -100,7 +94,7 @@ namespace osu.Game.Overlays.Changelog
}
};
protected override ScreenTitle CreateTitle() => title = new ChangelogHeaderTitle();
protected override OverlayTitle CreateTitle() => new ChangelogHeaderTitle();
public void Populate(List<APIUpdateStream> streams)
{
@ -116,20 +110,13 @@ namespace osu.Game.Overlays.Changelog
Streams.Current.Value = Streams.Items.FirstOrDefault(s => s.Name == Build.Value.UpdateStream.Name);
}
private class ChangelogHeaderTitle : ScreenTitle
private class ChangelogHeaderTitle : OverlayTitle
{
public string Version
{
set => Section = value ?? listing_string;
}
public ChangelogHeaderTitle()
{
Title = "changelog";
Version = null;
IconTexture = "Icons/changelog";
}
protected override Drawable CreateIcon() => new ScreenTitleTextureIcon(@"Icons/changelog");
}
}
}

View File

@ -29,14 +29,8 @@ namespace osu.Game.Overlays.Music
{
public CollectionsMenu()
{
Masking = true;
CornerRadius = 5;
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Colour = Color4.Black.Opacity(0.3f),
Radius = 3,
Offset = new Vector2(0f, 1f),
};
}
[BackgroundDependencyLoader]

View File

@ -3,7 +3,6 @@
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Graphics.UserInterface;
using System;
namespace osu.Game.Overlays.News
@ -12,8 +11,6 @@ namespace osu.Game.Overlays.News
{
private const string front_page_string = "frontpage";
private NewsHeaderTitle title;
public readonly Bindable<string> Post = new Bindable<string>(null);
public Action ShowFrontPage;
@ -40,36 +37,24 @@ namespace osu.Game.Overlays.News
{
TabControl.AddItem(e.NewValue);
Current.Value = e.NewValue;
title.IsReadingPost = true;
}
else
{
Current.Value = front_page_string;
title.IsReadingPost = false;
}
}
protected override Drawable CreateBackground() => new OverlayHeaderBackground(@"Headers/news");
protected override ScreenTitle CreateTitle() => title = new NewsHeaderTitle();
protected override OverlayTitle CreateTitle() => new NewsHeaderTitle();
private class NewsHeaderTitle : ScreenTitle
private class NewsHeaderTitle : OverlayTitle
{
private const string post_string = "post";
public bool IsReadingPost
{
set => Section = value ? post_string : front_page_string;
}
public NewsHeaderTitle()
{
Title = "news";
IsReadingPost = false;
IconTexture = "Icons/news";
}
protected override Drawable CreateIcon() => new ScreenTitleTextureIcon(@"Icons/news");
}
}
}

View File

@ -6,7 +6,6 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.UserInterface;
using osuTK.Graphics;
namespace osu.Game.Overlays
@ -14,7 +13,6 @@ namespace osu.Game.Overlays
public abstract class OverlayHeader : Container
{
private readonly Box titleBackground;
private readonly ScreenTitle title;
protected readonly FillFlowContainer HeaderInfo;
@ -57,11 +55,10 @@ namespace osu.Game.Overlays
Padding = new MarginPadding
{
Horizontal = UserProfileOverlay.CONTENT_X_MARGIN,
Vertical = 10,
},
Children = new[]
{
title = CreateTitle().With(title =>
CreateTitle().With(title =>
{
title.Anchor = Anchor.CentreLeft;
title.Origin = Anchor.CentreLeft;
@ -86,7 +83,6 @@ namespace osu.Game.Overlays
private void load(OverlayColourProvider colourProvider)
{
titleBackground.Colour = colourProvider.Dark5;
title.AccentColour = colourProvider.Highlight1;
}
[NotNull]
@ -96,11 +92,11 @@ namespace osu.Game.Overlays
protected virtual Drawable CreateBackground() => Empty();
/// <summary>
/// Creates a <see cref="Drawable"/> on the opposite side of the <see cref="ScreenTitle"/>. Used mostly to create <see cref="OverlayRulesetSelector"/>.
/// Creates a <see cref="Drawable"/> on the opposite side of the <see cref="OverlayTitle"/>. Used mostly to create <see cref="OverlayRulesetSelector"/>.
/// </summary>
[NotNull]
protected virtual Drawable CreateTitleContent() => Empty();
protected abstract ScreenTitle CreateTitle();
protected abstract OverlayTitle CreateTitle();
}
}

View File

@ -22,7 +22,7 @@ namespace osu.Game.Overlays
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(25, 0),
Spacing = new Vector2(20, 0),
};
}
}

View File

@ -12,6 +12,7 @@ using osu.Game.Rulesets;
using osuTK.Graphics;
using osuTK;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
namespace osu.Game.Overlays
{
@ -53,6 +54,8 @@ namespace osu.Game.Overlays
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Text = value.Name,
Font = OsuFont.GetFont(size: 14),
ShadowColour = Color4.Black.Opacity(0.75f)
}
},
new HoverClickSounds()

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
@ -35,17 +36,22 @@ namespace osu.Game.Overlays
protected OverlayTabControl()
{
TabContainer.Masking = false;
TabContainer.Spacing = new Vector2(15, 0);
TabContainer.Spacing = new Vector2(20, 0);
AddInternal(bar = new Box
{
RelativeSizeAxes = Axes.X,
Height = 2,
Anchor = Anchor.BottomLeft,
Origin = Anchor.CentreLeft
Origin = Anchor.BottomLeft
});
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
AccentColour = colourProvider.Highlight1;
}
protected override Dropdown<T> CreateDropdown() => null;
protected override TabItem<T> CreateTabItem(T value) => new OverlayTabItem(value);
@ -90,7 +96,7 @@ namespace osu.Game.Overlays
Bar = new ExpandingBar
{
Anchor = Anchor.BottomCentre,
ExpandedSize = 7.5f,
ExpandedSize = 5f,
CollapsedSize = 0
},
new HoverClickSounds()
@ -119,6 +125,7 @@ namespace osu.Game.Overlays
{
HoverAction();
Text.Font = Text.Font.With(weight: FontWeight.Bold);
Text.FadeColour(Color4.White, 120, Easing.InQuad);
}
protected override void OnDeactivated()
@ -135,11 +142,7 @@ namespace osu.Game.Overlays
OnDeactivated();
}
protected virtual void HoverAction()
{
Bar.Expand();
Text.FadeColour(Color4.White, 120, Easing.InQuad);
}
protected virtual void HoverAction() => Bar.Expand();
protected virtual void UnhoverAction()
{

View File

@ -0,0 +1,80 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK;
namespace osu.Game.Overlays
{
public abstract class OverlayTitle : CompositeDrawable
{
private readonly OsuSpriteText title;
private readonly Container icon;
protected string Title
{
set => title.Text = value;
}
protected string IconTexture
{
set => icon.Child = new OverlayTitleIcon(value);
}
protected OverlayTitle()
{
AutoSizeAxes = Axes.Both;
InternalChild = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(10, 0),
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
icon = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Margin = new MarginPadding { Horizontal = 5 }, // compensates for osu-web sprites having around 5px of whitespace on each side
Size = new Vector2(30)
},
title = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.GetFont(size: 20, weight: FontWeight.Regular),
Margin = new MarginPadding { Vertical = 17.5f } // 15px padding + 2.5px line-height difference compensation
}
}
};
}
private class OverlayTitleIcon : Sprite
{
private readonly string textureName;
public OverlayTitleIcon(string textureName)
{
this.textureName = textureName;
RelativeSizeAxes = Axes.Both;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
FillMode = FillMode.Fit;
}
[BackgroundDependencyLoader]
private void load(TextureStore textures)
{
Texture = textures.Get(textureName);
}
}
}
}

View File

@ -7,7 +7,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Profile.Header;
using osu.Game.Users;
@ -87,19 +86,17 @@ namespace osu.Game.Overlays.Profile
}
};
protected override ScreenTitle CreateTitle() => new ProfileHeaderTitle();
protected override OverlayTitle CreateTitle() => new ProfileHeaderTitle();
private void updateDisplay(User user) => coverContainer.User = user;
private class ProfileHeaderTitle : ScreenTitle
private class ProfileHeaderTitle : OverlayTitle
{
public ProfileHeaderTitle()
{
Title = "player";
Section = "info";
Title = "player info";
IconTexture = "Icons/profile";
}
protected override Drawable CreateIcon() => new ScreenTitleTextureIcon(@"Icons/profile");
}
}
}

View File

@ -3,7 +3,6 @@
using osu.Framework.Graphics;
using osu.Framework.Bindables;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets;
using osu.Game.Users;
@ -18,33 +17,21 @@ namespace osu.Game.Overlays.Rankings
private OverlayRulesetSelector rulesetSelector;
private CountryFilter countryFilter;
protected override ScreenTitle CreateTitle() => new RankingsTitle
{
Scope = { BindTarget = Current }
};
protected override OverlayTitle CreateTitle() => new RankingsTitle();
protected override Drawable CreateTitleContent() => rulesetSelector = new OverlayRulesetSelector();
protected override Drawable CreateContent() => countryFilter = new CountryFilter();
protected override Drawable CreateBackground() => new OverlayHeaderBackground(@"Headers/rankings");
protected override Drawable CreateBackground() => new OverlayHeaderBackground("Headers/rankings");
private class RankingsTitle : ScreenTitle
private class RankingsTitle : OverlayTitle
{
public readonly Bindable<RankingsScope> Scope = new Bindable<RankingsScope>();
public RankingsTitle()
{
Title = "ranking";
IconTexture = "Icons/rankings";
}
protected override void LoadComplete()
{
base.LoadComplete();
Scope.BindValueChanged(scope => Section = scope.NewValue.ToString().ToLowerInvariant(), true);
}
protected override Drawable CreateIcon() => new ScreenTitleTextureIcon(@"Icons/rankings");
}
}

View File

@ -10,7 +10,6 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osuTK;
namespace osu.Game.Overlays
{
@ -22,6 +21,7 @@ namespace osu.Game.Overlays
{
protected OsuTabControl<T> TabControl;
private readonly Box controlBackground;
private readonly BindableWithCurrent<T> current = new BindableWithCurrent<T>();
public Bindable<T> Current
@ -30,8 +30,6 @@ namespace osu.Game.Overlays
set => current.Current = value;
}
private readonly Box controlBackground;
protected TabControlOverlayHeader()
{
HeaderInfo.Add(new Container
@ -56,7 +54,6 @@ namespace osu.Game.Overlays
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
TabControl.AccentColour = colourProvider.Highlight1;
controlBackground.Colour = colourProvider.Dark4;
}
@ -65,14 +62,16 @@ namespace osu.Game.Overlays
public class OverlayHeaderTabControl : OverlayTabControl<T>
{
private const float bar_height = 1;
public OverlayHeaderTabControl()
{
BarHeight = 1;
RelativeSizeAxes = Axes.None;
AutoSizeAxes = Axes.X;
Anchor = Anchor.BottomLeft;
Origin = Anchor.BottomLeft;
Height = 35;
Height = 47;
BarHeight = bar_height;
}
protected override TabItem<T> CreateTabItem(T value) => new OverlayHeaderTabItem(value);
@ -82,7 +81,6 @@ namespace osu.Game.Overlays
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5, 0),
};
private class OverlayHeaderTabItem : OverlayTabItem
@ -92,7 +90,8 @@ namespace osu.Game.Overlays
{
Text.Text = value.ToString().ToLower();
Text.Font = OsuFont.GetFont(size: 14);
Bar.ExpandedSize = 5;
Text.Margin = new MarginPadding { Vertical = 16.5f }; // 15px padding + 1.5px line-height difference compensation
Bar.Margin = new MarginPadding { Bottom = bar_height };
}
}
}

View File

@ -24,7 +24,7 @@ namespace osu.Game.Overlays
private GetUserRequest userReq;
protected ProfileHeader Header;
private ProfileSectionsContainer sectionsContainer;
private ProfileTabControl tabs;
private ProfileSectionTabControl tabs;
public const float CONTENT_X_MARGIN = 70;
@ -62,12 +62,11 @@ namespace osu.Game.Overlays
}
: Array.Empty<ProfileSection>();
tabs = new ProfileTabControl
tabs = new ProfileSectionTabControl
{
RelativeSizeAxes = Axes.X,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Height = 34
};
Add(new Box
@ -149,19 +148,24 @@ namespace osu.Game.Overlays
}
}
private class ProfileTabControl : OverlayTabControl<ProfileSection>
private class ProfileSectionTabControl : OverlayTabControl<ProfileSection>
{
public ProfileTabControl()
private const float bar_height = 2;
public ProfileSectionTabControl()
{
TabContainer.RelativeSizeAxes &= ~Axes.X;
TabContainer.AutoSizeAxes |= Axes.X;
TabContainer.Anchor |= Anchor.x1;
TabContainer.Origin |= Anchor.x1;
Height = 36 + bar_height;
BarHeight = bar_height;
}
protected override TabItem<ProfileSection> CreateTabItem(ProfileSection value) => new ProfileTabItem(value)
protected override TabItem<ProfileSection> CreateTabItem(ProfileSection value) => new ProfileSectionTabItem(value)
{
AccentColour = AccentColour
AccentColour = AccentColour,
};
[BackgroundDependencyLoader]
@ -170,12 +174,16 @@ namespace osu.Game.Overlays
AccentColour = colourProvider.Highlight1;
}
private class ProfileTabItem : OverlayTabItem
private class ProfileSectionTabItem : OverlayTabItem
{
public ProfileTabItem(ProfileSection value)
public ProfileSectionTabItem(ProfileSection value)
: base(value)
{
Text.Text = value.Title;
Text.Font = Text.Font.With(size: 16);
Text.Margin = new MarginPadding { Bottom = 10 + bar_height };
Bar.ExpandedSize = 10;
Bar.Margin = new MarginPadding { Bottom = bar_height };
}
}
}

View File

@ -69,10 +69,6 @@ namespace osu.Game.Rulesets.Edit
[BackgroundDependencyLoader]
private void load(IFrameBasedClock framedClock)
{
EditorBeatmap.HitObjectAdded += addHitObject;
EditorBeatmap.HitObjectRemoved += removeHitObject;
EditorBeatmap.StartTimeChanged += UpdateHitObject;
Config = Dependencies.Get<RulesetConfigCache>().GetConfigFor(Ruleset);
try
@ -236,10 +232,6 @@ namespace osu.Game.Rulesets.Edit
lastGridUpdateTime = EditorClock.CurrentTime;
}
private void addHitObject(HitObject hitObject) => UpdateHitObject(hitObject);
private void removeHitObject(HitObject hitObject) => UpdateHitObject(null);
public override IEnumerable<DrawableHitObject> HitObjects => drawableRulesetWrapper.Playfield.AllHitObjects;
public override bool CursorInPlacementArea => drawableRulesetWrapper.Playfield.ReceivePositionalInputAt(inputManager.CurrentState.Mouse.Position);
@ -302,19 +294,6 @@ namespace osu.Game.Rulesets.Edit
return DurationToDistance(referenceTime, snappedEndTime - referenceTime);
}
public override void UpdateHitObject(HitObject hitObject) => EditorBeatmap.UpdateHitObject(hitObject);
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (EditorBeatmap != null)
{
EditorBeatmap.HitObjectAdded -= addHitObject;
EditorBeatmap.HitObjectRemoved -= removeHitObject;
}
}
}
[Cached(typeof(HitObjectComposer))]
@ -344,12 +323,6 @@ namespace osu.Game.Rulesets.Edit
[CanBeNull]
protected virtual DistanceSnapGrid CreateDistanceSnapGrid([NotNull] IEnumerable<HitObject> selectedHitObjects) => null;
/// <summary>
/// Updates a <see cref="HitObject"/>, invoking <see cref="HitObject.ApplyDefaults"/> and re-processing the beatmap.
/// </summary>
/// <param name="hitObject">The <see cref="HitObject"/> to update.</param>
public abstract void UpdateHitObject([CanBeNull] HitObject hitObject);
public abstract (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time);
public abstract float GetBeatSnapDistanceAt(double referenceTime);

View File

@ -108,11 +108,6 @@ namespace osu.Game.Rulesets.Edit
public bool IsSelected => State == SelectionState.Selected;
/// <summary>
/// Updates the <see cref="Objects.HitObject"/>, invoking <see cref="Objects.HitObject.ApplyDefaults"/> and re-processing the beatmap.
/// </summary>
protected void UpdateHitObject() => composer?.UpdateHitObject(HitObject);
/// <summary>
/// The <see cref="MenuItem"/>s to be displayed in the context menu for this <see cref="OverlaySelectionBlueprint"/>.
/// </summary>

View File

@ -375,7 +375,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
}
}
protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => AllJudged && base.ComputeIsMaskedAway(maskingBounds);
public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) => AllJudged && base.UpdateSubTreeMasking(source, maskingBounds);
protected override void UpdateAfterChildren()
{

View File

@ -60,8 +60,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
waveform.Waveform = b.NewValue.Waveform;
track = b.NewValue.Track;
MinZoom = getZoomLevelForVisibleMilliseconds(10000);
MaxZoom = getZoomLevelForVisibleMilliseconds(500);
MinZoom = getZoomLevelForVisibleMilliseconds(10000);
Zoom = getZoomLevelForVisibleMilliseconds(2000);
}, true);
}

View File

@ -4,6 +4,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@ -29,9 +30,9 @@ namespace osu.Game.Screens.Edit
public event Action<HitObject> HitObjectRemoved;
/// <summary>
/// Invoked when the start time of a <see cref="HitObject"/> in this <see cref="EditorBeatmap"/> was changed.
/// Invoked when a <see cref="HitObject"/> is updated.
/// </summary>
public event Action<HitObject> StartTimeChanged;
public event Action<HitObject> HitObjectUpdated;
/// <summary>
/// All currently selected <see cref="HitObject"/>s.
@ -62,21 +63,39 @@ namespace osu.Game.Screens.Edit
trackStartTime(obj);
}
private readonly HashSet<HitObject> pendingUpdates = new HashSet<HitObject>();
private ScheduledDelegate scheduledUpdate;
/// <summary>
/// Updates a <see cref="HitObject"/>, invoking <see cref="HitObject.ApplyDefaults"/> and re-processing the beatmap.
/// </summary>
/// <param name="hitObject">The <see cref="HitObject"/> to update.</param>
public void UpdateHitObject(HitObject hitObject)
public void UpdateHitObject([NotNull] HitObject hitObject) => updateHitObject(hitObject, false);
private void updateHitObject([CanBeNull] HitObject hitObject, bool silent)
{
scheduledUpdate?.Cancel();
scheduledUpdate = Scheduler.AddDelayed(() =>
if (hitObject != null)
pendingUpdates.Add(hitObject);
scheduledUpdate = Schedule(() =>
{
beatmapProcessor?.PreProcess();
hitObject?.ApplyDefaults(ControlPointInfo, BeatmapInfo.BaseDifficulty);
foreach (var obj in pendingUpdates)
obj.ApplyDefaults(ControlPointInfo, BeatmapInfo.BaseDifficulty);
beatmapProcessor?.PostProcess();
}, 0);
if (!silent)
{
foreach (var obj in pendingUpdates)
HitObjectUpdated?.Invoke(obj);
}
pendingUpdates.Clear();
});
}
public BeatmapInfo BeatmapInfo
@ -114,6 +133,8 @@ namespace osu.Game.Screens.Edit
mutableHitObjects.Insert(insertionIndex + 1, hitObject);
HitObjectAdded?.Invoke(hitObject);
updateHitObject(hitObject, true);
}
/// <summary>
@ -132,6 +153,8 @@ namespace osu.Game.Screens.Edit
startTimeBindables.Remove(hitObject);
HitObjectRemoved?.Invoke(hitObject);
updateHitObject(null, true);
}
private void trackStartTime(HitObject hitObject)
@ -145,7 +168,7 @@ namespace osu.Game.Screens.Edit
var insertionIndex = findInsertionIndex(PlayableBeatmap.HitObjects, hitObject.StartTime);
mutableHitObjects.Insert(insertionIndex + 1, hitObject);
StartTimeChanged?.Invoke(hitObject);
UpdateHitObject(hitObject);
};
}

View File

@ -270,7 +270,7 @@ namespace osu.Game.Screens.Menu
[BackgroundDependencyLoader]
private void load()
{
InternalChild = new Video(videoStream, false)
InternalChild = new Video(videoStream)
{
RelativeSizeAxes = Axes.Both,
};

View File

@ -6,10 +6,13 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Screens;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.SearchableList;
using osu.Game.Graphics.Sprites;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Multi
@ -43,7 +46,7 @@ namespace osu.Game.Screens.Multi
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.BottomLeft,
X = -ScreenTitle.ICON_WIDTH,
X = -MultiHeaderTitle.ICON_WIDTH,
},
breadcrumbs = new HeaderBreadcrumbControl(stack)
{
@ -70,18 +73,78 @@ namespace osu.Game.Screens.Multi
breadcrumbs.StripColour = colours.Green;
}
private class MultiHeaderTitle : ScreenTitle
private class MultiHeaderTitle : CompositeDrawable, IHasAccentColour
{
public const float ICON_WIDTH = icon_size + spacing;
private const float icon_size = 25;
private const float spacing = 6;
private const int text_offset = 2;
private readonly SpriteIcon iconSprite;
private readonly OsuSpriteText title, pageText;
public IMultiplayerSubScreen Screen
{
set => Section = value.ShortTitle.ToLowerInvariant();
set => pageText.Text = value.ShortTitle.ToLowerInvariant();
}
public Color4 AccentColour
{
get => pageText.Colour;
set => pageText.Colour = value;
}
public MultiHeaderTitle()
{
AutoSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(spacing, 0),
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
iconSprite = new SpriteIcon
{
Size = new Vector2(icon_size),
Anchor = Anchor.Centre,
Origin = Anchor.Centre
},
title = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.GetFont(size: 20, weight: FontWeight.Bold),
Margin = new MarginPadding { Bottom = text_offset }
},
new Circle
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(4),
Colour = Color4.Gray,
},
pageText = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.GetFont(size: 20),
Margin = new MarginPadding { Bottom = text_offset }
}
}
},
};
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Title = "multi";
Icon = OsuIcon.Multi;
title.Text = "multi";
iconSprite.Icon = OsuIcon.Multi;
AccentColour = colours.Yellow;
}
}

View File

@ -122,10 +122,24 @@ namespace osu.Game.Screens.Select.Carousel
},
}
},
starCounter = new StarCounter
new FillFlowContainer
{
Current = (float)beatmap.StarDifficulty,
Scale = new Vector2(0.8f),
Direction = FillDirection.Horizontal,
Spacing = new Vector2(4, 0),
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
new TopLocalRank(beatmap)
{
Scale = new Vector2(0.8f),
Size = new Vector2(40, 20)
},
starCounter = new StarCounter
{
Current = (float)beatmap.StarDifficulty,
Scale = new Vector2(0.8f),
}
}
}
}
}

View File

@ -0,0 +1,90 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Online.Leaderboards;
using osu.Game.Rulesets;
using osu.Game.Scoring;
namespace osu.Game.Screens.Select.Carousel
{
public class TopLocalRank : UpdateableRank
{
private readonly BeatmapInfo beatmap;
[Resolved]
private ScoreManager scores { get; set; }
[Resolved]
private IBindable<RulesetInfo> ruleset { get; set; }
[Resolved]
private IAPIProvider api { get; set; }
public TopLocalRank(BeatmapInfo beatmap)
: base(null)
{
this.beatmap = beatmap;
}
[BackgroundDependencyLoader]
private void load()
{
scores.ItemAdded += scoreChanged;
scores.ItemRemoved += scoreChanged;
ruleset.ValueChanged += _ => fetchAndLoadTopScore();
fetchAndLoadTopScore();
}
private void scoreChanged(ScoreInfo score)
{
if (score.BeatmapInfoID == beatmap.ID)
fetchAndLoadTopScore();
}
private ScheduledDelegate scheduledRankUpdate;
private void fetchAndLoadTopScore()
{
var rank = fetchTopScore()?.Rank;
scheduledRankUpdate = Schedule(() =>
{
Rank = rank;
// Required since presence is changed via IsPresent override
Invalidate(Invalidation.Presence);
});
}
// We're present if a rank is set, or if there is a pending rank update (IsPresent = true is required for the scheduler to run).
public override bool IsPresent => base.IsPresent && (Rank != null || scheduledRankUpdate?.Completed == false);
private ScoreInfo fetchTopScore()
{
if (scores == null || beatmap == null || ruleset?.Value == null || api?.LocalUser.Value == null)
return null;
return scores.QueryScores(s => s.UserID == api.LocalUser.Value.Id && s.BeatmapInfoID == beatmap.ID && s.RulesetID == ruleset.Value.ID && !s.DeletePending)
.OrderByDescending(s => s.TotalScore)
.FirstOrDefault();
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (scores != null)
{
scores.ItemAdded -= scoreChanged;
scores.ItemRemoved -= scoreChanged;
}
}
}
}

View File

@ -20,7 +20,7 @@ namespace osu.Game.Skinning
new Color4(242, 24, 57, 255)
);
Configuration.LegacyVersion = 2.0m;
Configuration.LegacyVersion = 2.7m;
}
public static SkinInfo Info { get; } = new SkinInfo

View File

@ -40,6 +40,9 @@ namespace osu.Game.Skinning
JudgementLineColour,
ColumnBackgroundColour,
ColumnLightColour,
MinimumColumnWidth
MinimumColumnWidth,
LeftStageImage,
RightStageImage,
BottomStageImage
}
}

View File

@ -102,7 +102,9 @@ namespace osu.Game.Skinning
break;
case "WidthForNoteHeightScale":
currentConfig.MinimumColumnWidth = float.Parse(pair.Value, CultureInfo.InvariantCulture) * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR;
float minWidth = float.Parse(pair.Value, CultureInfo.InvariantCulture) * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR;
if (minWidth > 0)
currentConfig.MinimumColumnWidth = minWidth;
break;
case string _ when pair.Key.StartsWith("Colour"):

View File

@ -243,6 +243,12 @@ namespace osu.Game.Skinning
case LegacyManiaSkinConfigurationLookups.KeyImageDown:
Debug.Assert(maniaLookup.TargetColumn != null);
return SkinUtils.As<TValue>(getManiaImage(existing, $"KeyImage{maniaLookup.TargetColumn}D"));
case LegacyManiaSkinConfigurationLookups.LeftStageImage:
return SkinUtils.As<TValue>(getManiaImage(existing, "StageLeft"));
case LegacyManiaSkinConfigurationLookups.RightStageImage:
return SkinUtils.As<TValue>(getManiaImage(existing, "StageRight"));
}
return null;

View File

@ -22,7 +22,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2020.407.1" />
<PackageReference Include="ppy.osu.Framework" Version="2020.411.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.403.0" />
<PackageReference Include="Sentry" Version="2.1.1" />
<PackageReference Include="SharpCompress" Version="0.25.0" />

View File

@ -70,7 +70,7 @@
<Reference Include="System.Net.Http" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.407.1" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.411.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.403.0" />
</ItemGroup>
<!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. -->
@ -79,7 +79,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2020.407.1" />
<PackageReference Include="ppy.osu.Framework" Version="2020.411.0" />
<PackageReference Include="SharpCompress" Version="0.25.0" />
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="SharpRaven" Version="2.4.0" />