1
0
mirror of https://github.com/ppy/osu.git synced 2025-03-12 10:27:20 +08:00

Merge branch 'master' into realm-key-binding-store

This commit is contained in:
Dean Herbert 2021-06-18 16:52:35 +09:00
commit c369beeaaa
87 changed files with 1368 additions and 544 deletions

View File

@ -14,8 +14,8 @@
"jb" "jb"
] ]
}, },
"nvika": { "smoogipoo.nvika": {
"version": "2.0.0", "version": "1.0.1",
"commands": [ "commands": [
"nvika" "nvika"
] ]

93
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,93 @@
on: [push, pull_request]
name: Continuous Integration
jobs:
test:
name: Test
runs-on: ${{matrix.os.fullname}}
env:
OSU_EXECUTION_MODE: ${{matrix.threadingMode}}
strategy:
fail-fast: false
matrix:
os:
- { prettyname: Windows, fullname: windows-latest }
- { prettyname: macOS, fullname: macos-latest }
- { prettyname: Linux, fullname: ubuntu-latest }
threadingMode: ['SingleThread', 'MultiThreaded']
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Install .NET 5.0.x
uses: actions/setup-dotnet@v1
with:
dotnet-version: "5.0.x"
# FIXME: libavformat is not included in Ubuntu. Let's fix that.
# https://github.com/ppy/osu-framework/issues/4349
# Remove this once https://github.com/actions/virtual-environments/issues/3306 has been resolved.
- name: Install libavformat-dev
if: ${{matrix.os.fullname == 'ubuntu-latest'}}
run: |
sudo apt-get update && \
sudo apt-get -y install libavformat-dev
- name: Compile
run: dotnet build -c Debug -warnaserror osu.Desktop.slnf
- name: Test
run: dotnet test $pwd/*.Tests/bin/Debug/*/*.Tests.dll --logger "trx;LogFileName=TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx"
shell: pwsh
# Attempt to upload results even if test fails.
# https://docs.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#always
- name: Upload Test Results
uses: actions/upload-artifact@v2
if: ${{ always() }}
with:
name: osu-test-results-${{matrix.os.prettyname}}-${{matrix.threadingMode}}
path: ${{github.workspace}}/TestResults/TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx
inspect-code:
name: Code Quality
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
# FIXME: Tools won't run in .NET 5.0 unless you install 3.1.x LTS side by side.
# https://itnext.io/how-to-support-multiple-net-sdks-in-github-actions-workflows-b988daa884e
- name: Install .NET 3.1.x LTS
uses: actions/setup-dotnet@v1
with:
dotnet-version: "3.1.x"
- name: Install .NET 5.0.x
uses: actions/setup-dotnet@v1
with:
dotnet-version: "5.0.x"
- name: Restore Tools
run: dotnet tool restore
- name: Restore Packages
run: dotnet restore
- name: CodeFileSanity
run: |
# TODO: Add ignore filters and GitHub Workflow Command Reporting in CFS. That way we don't have to do this workaround.
# FIXME: Suppress warnings from templates project
dotnet codefilesanity | while read -r line; do
echo "::warning::$line"
done
# Temporarily disabled due to test failures, but it won't work anyway until the tool is upgraded.
# - name: .NET Format (Dry Run)
# run: dotnet format --dry-run --check
- name: InspectCode
run: dotnet jb inspectcode $(pwd)/osu.Desktop.slnf --output=$(pwd)/inspectcodereport.xml --cachesDir=$(pwd)/inspectcode --verbosity=WARN
- name: NVika
run: dotnet nvika parsereport "${{github.workspace}}/inspectcodereport.xml" --treatwarningsaserrors

31
.github/workflows/report-nunit.yml vendored Normal file
View File

@ -0,0 +1,31 @@
# This is a workaround to allow PRs to report their coverage. This will run inside the base repository.
# See:
# * https://github.com/dorny/test-reporter#recommended-setup-for-public-repositories
# * https://docs.github.com/en/actions/reference/authentication-in-a-workflow#permissions-for-the-github_token
name: Annotate CI run with test results
on:
workflow_run:
workflows: ["Continuous Integration"]
types:
- completed
jobs:
annotate:
name: Annotate CI run with test results
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion != 'cancelled' }}
strategy:
fail-fast: false
matrix:
os:
- { prettyname: Windows }
- { prettyname: macOS }
- { prettyname: Linux }
threadingMode: ['SingleThread', 'MultiThreaded']
steps:
- name: Annotate CI run with test results
uses: dorny/test-reporter@v1.4.2
with:
artifact: osu-test-results-${{matrix.os.prettyname}}-${{matrix.threadingMode}}
name: Test Results (${{matrix.os.prettyname}}, ${{matrix.threadingMode}})
path: "*.trx"
reporter: dotnet-trx

View File

@ -51,7 +51,7 @@
<Reference Include="Java.Interop" /> <Reference Include="Java.Interop" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.611.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.614.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.614.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2021.616.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -174,8 +174,8 @@ namespace osu.Game.Rulesets.Catch.Tests
private void addToPlayfield(DrawableCatchHitObject drawable) private void addToPlayfield(DrawableCatchHitObject drawable)
{ {
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObjects>()) foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObject>())
mod.ApplyToDrawableHitObjects(new[] { drawable }); mod.ApplyToDrawableHitObject(drawable);
drawableRuleset.Playfield.Add(drawable); drawableRuleset.Playfield.Add(drawable);
} }

View File

@ -1,31 +1,54 @@
// 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.
using System.Collections.Generic;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Timing; using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Tests.Editor namespace osu.Game.Rulesets.Mania.Tests.Editor
{ {
public abstract class ManiaSelectionBlueprintTestScene : SelectionBlueprintTestScene public abstract class ManiaSelectionBlueprintTestScene : SelectionBlueprintTestScene
{ {
[Cached(Type = typeof(IAdjustableClock))] protected override Container<Drawable> Content => blueprints ?? base.Content;
private readonly IAdjustableClock clock = new StopwatchClock();
protected ManiaSelectionBlueprintTestScene() private readonly Container blueprints;
[Cached(typeof(Playfield))]
public Playfield Playfield { get; }
private readonly ScrollingTestContainer scrollingTestContainer;
protected ScrollingDirection Direction
{ {
Add(new Column(0) set => scrollingTestContainer.Direction = value;
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AccentColour = Color4.OrangeRed,
Clock = new FramedClock(new StopwatchClock()), // No scroll
});
} }
public ManiaPlayfield Playfield => null; protected ManiaSelectionBlueprintTestScene(int columns)
{
var stageDefinitions = new List<StageDefinition> { new StageDefinition { Columns = columns } };
base.Content.Child = scrollingTestContainer = new ScrollingTestContainer(ScrollingDirection.Up)
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
Playfield = new ManiaPlayfield(stageDefinitions)
{
RelativeSizeAxes = Axes.Both,
},
blueprints = new Container
{
RelativeSizeAxes = Axes.Both
}
}
};
AddToggleStep("Downward scroll", b => Direction = b ? ScrollingDirection.Down : ScrollingDirection.Up);
}
} }
} }

View File

@ -1,55 +1,32 @@
// 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.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Edit.Blueprints; using osu.Game.Rulesets.Mania.Edit.Blueprints;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests.Editor namespace osu.Game.Rulesets.Mania.Tests.Editor
{ {
public class TestSceneHoldNoteSelectionBlueprint : ManiaSelectionBlueprintTestScene public class TestSceneHoldNoteSelectionBlueprint : ManiaSelectionBlueprintTestScene
{ {
private readonly DrawableHoldNote drawableObject;
protected override Container<Drawable> Content => content ?? base.Content;
private readonly Container content;
public TestSceneHoldNoteSelectionBlueprint() public TestSceneHoldNoteSelectionBlueprint()
: base(4)
{ {
var holdNote = new HoldNote { Column = 0, Duration = 1000 }; for (int i = 0; i < 4; i++)
holdNote.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
base.Content.Child = content = new ScrollingTestContainer(ScrollingDirection.Down)
{ {
Anchor = Anchor.Centre, var holdNote = new HoldNote
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Y,
Width = 50,
Child = drawableObject = new DrawableHoldNote(holdNote)
{ {
Height = 300, Column = i,
AccentColour = { Value = OsuColour.Gray(0.3f) } StartTime = i * 100,
} Duration = 500
}; };
holdNote.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
AddBlueprint(new HoldNoteSelectionBlueprint(holdNote), drawableObject); var drawableHitObject = new DrawableHoldNote(holdNote);
} Playfield.Add(drawableHitObject);
AddBlueprint(new HoldNoteSelectionBlueprint(holdNote), drawableHitObject);
protected override void Update()
{
base.Update();
foreach (var nested in drawableObject.NestedHitObjects)
{
double finalPosition = (nested.HitObject.StartTime - drawableObject.HitObject.StartTime) / drawableObject.HitObject.Duration;
nested.Y = (float)(-finalPosition * content.DrawHeight);
} }
} }
} }

View File

@ -12,7 +12,7 @@ using osu.Framework.Utils;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Edit; using osu.Game.Rulesets.Mania.Edit;
using osu.Game.Rulesets.Mania.Edit.Blueprints; using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.Skinning.Default; using osu.Game.Rulesets.Mania.Skinning.Default;
@ -184,8 +184,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
AddAssert("head note positioned correctly", () => Precision.AlmostEquals(holdNote.ScreenSpaceDrawQuad.BottomLeft, holdNote.Head.ScreenSpaceDrawQuad.BottomLeft)); AddAssert("head note positioned correctly", () => Precision.AlmostEquals(holdNote.ScreenSpaceDrawQuad.BottomLeft, holdNote.Head.ScreenSpaceDrawQuad.BottomLeft));
AddAssert("tail note positioned correctly", () => Precision.AlmostEquals(holdNote.ScreenSpaceDrawQuad.TopLeft, holdNote.Tail.ScreenSpaceDrawQuad.BottomLeft)); AddAssert("tail note positioned correctly", () => Precision.AlmostEquals(holdNote.ScreenSpaceDrawQuad.TopLeft, holdNote.Tail.ScreenSpaceDrawQuad.BottomLeft));
AddAssert("head blueprint positioned correctly", () => this.ChildrenOfType<HoldNoteNoteOverlay>().ElementAt(0).DrawPosition == holdNote.Head.DrawPosition); AddAssert("head blueprint positioned correctly", () => this.ChildrenOfType<EditNotePiece>().ElementAt(0).DrawPosition == holdNote.Head.DrawPosition);
AddAssert("tail blueprint positioned correctly", () => this.ChildrenOfType<HoldNoteNoteOverlay>().ElementAt(1).DrawPosition == holdNote.Tail.DrawPosition); AddAssert("tail blueprint positioned correctly", () => this.ChildrenOfType<EditNotePiece>().ElementAt(1).DrawPosition == holdNote.Tail.DrawPosition);
} }
private void setScrollStep(ScrollingDirection direction) private void setScrollStep(ScrollingDirection direction)

View File

@ -1,40 +1,32 @@
// 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.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Mania.Edit.Blueprints; using osu.Game.Rulesets.Mania.Edit.Blueprints;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Mania.Tests.Editor namespace osu.Game.Rulesets.Mania.Tests.Editor
{ {
public class TestSceneNoteSelectionBlueprint : ManiaSelectionBlueprintTestScene public class TestSceneNoteSelectionBlueprint : ManiaSelectionBlueprintTestScene
{ {
protected override Container<Drawable> Content => content ?? base.Content;
private readonly Container content;
public TestSceneNoteSelectionBlueprint() public TestSceneNoteSelectionBlueprint()
: base(4)
{ {
var note = new Note { Column = 0 }; for (int i = 0; i < 4; i++)
note.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
DrawableNote drawableObject;
base.Content.Child = content = new ScrollingTestContainer(ScrollingDirection.Down)
{ {
Anchor = Anchor.Centre, var note = new Note
Origin = Anchor.Centre, {
Size = new Vector2(50, 20), Column = i,
Child = drawableObject = new DrawableNote(note) StartTime = i * 200,
}; };
note.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
AddBlueprint(new NoteSelectionBlueprint(note), drawableObject); var drawableHitObject = new DrawableNote(note);
Playfield.Add(drawableHitObject);
AddBlueprint(new NoteSelectionBlueprint(note), drawableHitObject);
}
} }
} }
} }

View File

@ -1,43 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
using osu.Game.Rulesets.Mania.Objects.Drawables;
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
public class HoldNoteNoteOverlay : CompositeDrawable
{
private readonly HoldNoteSelectionBlueprint holdNoteBlueprint;
private readonly HoldNotePosition position;
public HoldNoteNoteOverlay(HoldNoteSelectionBlueprint holdNoteBlueprint, HoldNotePosition position)
{
this.holdNoteBlueprint = holdNoteBlueprint;
this.position = position;
InternalChild = new EditNotePiece { RelativeSizeAxes = Axes.X };
}
protected override void Update()
{
base.Update();
var drawableObject = holdNoteBlueprint.DrawableObject;
// Todo: This shouldn't exist, mania should not reference the drawable hitobject directly.
if (drawableObject.IsLoaded)
{
DrawableNote note = position == HoldNotePosition.Start ? (DrawableNote)drawableObject.Head : drawableObject.Tail;
Anchor = note.Anchor;
Origin = note.Origin;
Size = note.DrawSize;
Position = note.DrawPosition;
}
}
}
}

View File

@ -1,11 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
public enum HoldNotePosition
{
Start,
End
}
}

View File

@ -2,14 +2,13 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
using osuTK; using osuTK;
@ -17,13 +16,12 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{ {
public class HoldNoteSelectionBlueprint : ManiaSelectionBlueprint<HoldNote> public class HoldNoteSelectionBlueprint : ManiaSelectionBlueprint<HoldNote>
{ {
public new DrawableHoldNote DrawableObject => (DrawableHoldNote)base.DrawableObject;
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
[Resolved] [Resolved]
private OsuColour colours { get; set; } private OsuColour colours { get; set; }
private EditNotePiece head;
private EditNotePiece tail;
public HoldNoteSelectionBlueprint(HoldNote hold) public HoldNoteSelectionBlueprint(HoldNote hold)
: base(hold) : base(hold)
{ {
@ -32,12 +30,10 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(IScrollingInfo scrollingInfo) private void load(IScrollingInfo scrollingInfo)
{ {
direction.BindTo(scrollingInfo.Direction);
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
new HoldNoteNoteOverlay(this, HoldNotePosition.Start), head = new EditNotePiece { RelativeSizeAxes = Axes.X },
new HoldNoteNoteOverlay(this, HoldNotePosition.End), tail = new EditNotePiece { RelativeSizeAxes = Axes.X },
new Container new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
@ -58,21 +54,13 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{ {
base.Update(); base.Update();
// Todo: This shouldn't exist, mania should not reference the drawable hitobject directly. head.Y = HitObjectContainer.PositionAtTime(HitObject.Head.StartTime, HitObject.StartTime);
if (DrawableObject.IsLoaded) tail.Y = HitObjectContainer.PositionAtTime(HitObject.Tail.StartTime, HitObject.StartTime);
{ Height = HitObjectContainer.LengthAtTime(HitObject.StartTime, HitObject.EndTime) + tail.DrawHeight;
Size = DrawableObject.DrawSize + new Vector2(0, DrawableObject.Tail.DrawHeight);
// This is a side-effect of not matching the hitobject's anchors/origins, which is kinda hard to do
// When scrolling upwards our origin is already at the top of the head note (which is the intended location),
// but when scrolling downwards our origin is at the _bottom_ of the tail note (where we need to be at the _top_ of the tail note)
if (direction.Value == ScrollingDirection.Down)
Y -= DrawableObject.Tail.DrawHeight;
}
} }
public override Quad SelectionQuad => ScreenSpaceDrawQuad; public override Quad SelectionQuad => ScreenSpaceDrawQuad;
public override Vector2 ScreenSpaceSelectionPoint => DrawableObject.Head.ScreenSpaceDrawQuad.Centre; public override Vector2 ScreenSpaceSelectionPoint => head.ScreenSpaceDrawQuad.Centre;
} }
} }

View File

@ -5,20 +5,23 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
namespace osu.Game.Rulesets.Mania.Edit.Blueprints namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{ {
public abstract class ManiaSelectionBlueprint<T> : HitObjectSelectionBlueprint<T> public abstract class ManiaSelectionBlueprint<T> : HitObjectSelectionBlueprint<T>
where T : ManiaHitObject where T : ManiaHitObject
{ {
public new DrawableManiaHitObject DrawableObject => (DrawableManiaHitObject)base.DrawableObject; [Resolved]
private Playfield playfield { get; set; }
[Resolved] [Resolved]
private IScrollingInfo scrollingInfo { get; set; } private IScrollingInfo scrollingInfo { get; set; }
protected ScrollingHitObjectContainer HitObjectContainer => ((ManiaPlayfield)playfield).GetColumn(HitObject.Column).HitObjectContainer;
protected ManiaSelectionBlueprint(T hitObject) protected ManiaSelectionBlueprint(T hitObject)
: base(hitObject) : base(hitObject)
{ {
@ -29,19 +32,13 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{ {
base.Update(); base.Update();
Position = Parent.ToLocalSpace(DrawableObject.ToScreenSpace(Vector2.Zero)); var anchor = scrollingInfo.Direction.Value == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
} Anchor = Origin = anchor;
foreach (var child in InternalChildren)
child.Anchor = child.Origin = anchor;
public override void Show() Position = Parent.ToLocalSpace(HitObjectContainer.ScreenSpacePositionAtTime(HitObject.StartTime)) - AnchorPosition;
{ Width = HitObjectContainer.DrawWidth;
DrawableObject.AlwaysAlive = true;
base.Show();
}
public override void Hide()
{
DrawableObject.AlwaysAlive = false;
base.Hide();
} }
} }
} }

View File

@ -14,14 +14,5 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{ {
AddInternal(new EditNotePiece { RelativeSizeAxes = Axes.X }); AddInternal(new EditNotePiece { RelativeSizeAxes = Axes.X });
} }
protected override void Update()
{
base.Update();
// Todo: This shouldn't exist, mania should not reference the drawable hitobject directly.
if (DrawableObject.IsLoaded)
Size = DrawableObject.DrawSize;
}
} }
} }

View File

@ -85,63 +85,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
AccentColour.UnbindFrom(ParentHitObject.AccentColour); AccentColour.UnbindFrom(ParentHitObject.AccentColour);
} }
private double computedLifetimeStart;
public override double LifetimeStart
{
get => base.LifetimeStart;
set
{
computedLifetimeStart = value;
if (!AlwaysAlive)
base.LifetimeStart = value;
}
}
private double computedLifetimeEnd;
public override double LifetimeEnd
{
get => base.LifetimeEnd;
set
{
computedLifetimeEnd = value;
if (!AlwaysAlive)
base.LifetimeEnd = value;
}
}
private bool alwaysAlive;
/// <summary>
/// Whether this <see cref="DrawableManiaHitObject"/> should always remain alive.
/// </summary>
internal bool AlwaysAlive
{
get => alwaysAlive;
set
{
if (alwaysAlive == value)
return;
alwaysAlive = value;
if (value)
{
// Set the base lifetimes directly, to avoid mangling the computed lifetimes
base.LifetimeStart = double.MinValue;
base.LifetimeEnd = double.MaxValue;
}
else
{
LifetimeStart = computedLifetimeStart;
LifetimeEnd = computedLifetimeEnd;
}
}
}
protected virtual void OnDirectionChanged(ValueChangedEvent<ScrollingDirection> e) protected virtual void OnDirectionChanged(ValueChangedEvent<ScrollingDirection> e)
{ {
Anchor = Origin = e.NewValue == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre; Anchor = Origin = e.NewValue == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;

View File

@ -21,20 +21,37 @@ namespace osu.Game.Rulesets.Osu.Tests
private int depthIndex; private int depthIndex;
[Test] [Test]
public void TestVariousHitCircles() public void TestHits()
{
AddStep("Hit Big Single", () => SetContents(_ => testSingle(2, true)));
AddStep("Hit Medium Single", () => SetContents(_ => testSingle(5, true)));
AddStep("Hit Small Single", () => SetContents(_ => testSingle(7, true)));
AddStep("Hit Big Stream", () => SetContents(_ => testStream(2, true)));
AddStep("Hit Medium Stream", () => SetContents(_ => testStream(5, true)));
AddStep("Hit Small Stream", () => SetContents(_ => testStream(7, true)));
}
[Test]
public void TestHittingEarly()
{
AddStep("Hit stream early", () => SetContents(_ => testStream(5, true, -150)));
}
[Test]
public void TestMisses()
{ {
AddStep("Miss Big Single", () => SetContents(_ => testSingle(2))); AddStep("Miss Big Single", () => SetContents(_ => testSingle(2)));
AddStep("Miss Medium Single", () => SetContents(_ => testSingle(5))); AddStep("Miss Medium Single", () => SetContents(_ => testSingle(5)));
AddStep("Miss Small Single", () => SetContents(_ => testSingle(7))); AddStep("Miss Small Single", () => SetContents(_ => testSingle(7)));
AddStep("Hit Big Single", () => SetContents(_ => testSingle(2, true)));
AddStep("Hit Medium Single", () => SetContents(_ => testSingle(5, true)));
AddStep("Hit Small Single", () => SetContents(_ => testSingle(7, true)));
AddStep("Miss Big Stream", () => SetContents(_ => testStream(2))); AddStep("Miss Big Stream", () => SetContents(_ => testStream(2)));
AddStep("Miss Medium Stream", () => SetContents(_ => testStream(5))); AddStep("Miss Medium Stream", () => SetContents(_ => testStream(5)));
AddStep("Miss Small Stream", () => SetContents(_ => testStream(7))); AddStep("Miss Small Stream", () => SetContents(_ => testStream(7)));
AddStep("Hit Big Stream", () => SetContents(_ => testStream(2, true))); }
AddStep("Hit Medium Stream", () => SetContents(_ => testStream(5, true)));
AddStep("Hit Small Stream", () => SetContents(_ => testStream(7, true))); [Test]
public void TestHittingLate()
{
AddStep("Hit stream late", () => SetContents(_ => testStream(5, true, 150)));
} }
private Drawable testSingle(float circleSize, bool auto = false, double timeOffset = 0, Vector2? positionOffset = null) private Drawable testSingle(float circleSize, bool auto = false, double timeOffset = 0, Vector2? positionOffset = null)
@ -46,7 +63,7 @@ namespace osu.Game.Rulesets.Osu.Tests
return playfield; return playfield;
} }
private Drawable testStream(float circleSize, bool auto = false) private Drawable testStream(float circleSize, bool auto = false, double hitOffset = 0)
{ {
var playfield = new TestOsuPlayfield(); var playfield = new TestOsuPlayfield();
@ -54,14 +71,14 @@ namespace osu.Game.Rulesets.Osu.Tests
for (int i = 0; i <= 1000; i += 100) for (int i = 0; i <= 1000; i += 100)
{ {
playfield.Add(createSingle(circleSize, auto, i, pos)); playfield.Add(createSingle(circleSize, auto, i, pos, hitOffset));
pos.X += 50; pos.X += 50;
} }
return playfield; return playfield;
} }
private TestDrawableHitCircle createSingle(float circleSize, bool auto, double timeOffset, Vector2? positionOffset) private TestDrawableHitCircle createSingle(float circleSize, bool auto, double timeOffset, Vector2? positionOffset, double hitOffset = 0)
{ {
positionOffset ??= Vector2.Zero; positionOffset ??= Vector2.Zero;
@ -73,14 +90,14 @@ namespace osu.Game.Rulesets.Osu.Tests
circle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = circleSize }); circle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = circleSize });
var drawable = CreateDrawableHitCircle(circle, auto); var drawable = CreateDrawableHitCircle(circle, auto, hitOffset);
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObjects>()) foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObject>())
mod.ApplyToDrawableHitObjects(new[] { drawable }); mod.ApplyToDrawableHitObject(drawable);
return drawable; return drawable;
} }
protected virtual TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto) => new TestDrawableHitCircle(circle, auto) protected virtual TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto, double hitOffset = 0) => new TestDrawableHitCircle(circle, auto, hitOffset)
{ {
Depth = depthIndex++ Depth = depthIndex++
}; };
@ -88,18 +105,20 @@ namespace osu.Game.Rulesets.Osu.Tests
protected class TestDrawableHitCircle : DrawableHitCircle protected class TestDrawableHitCircle : DrawableHitCircle
{ {
private readonly bool auto; private readonly bool auto;
private readonly double hitOffset;
public TestDrawableHitCircle(HitCircle h, bool auto) public TestDrawableHitCircle(HitCircle h, bool auto, double hitOffset)
: base(h) : base(h)
{ {
this.auto = auto; this.auto = auto;
this.hitOffset = hitOffset;
} }
public void TriggerJudgement() => UpdateResult(true); public void TriggerJudgement() => Schedule(() => UpdateResult(true));
protected override void CheckForResult(bool userTriggered, double timeOffset) protected override void CheckForResult(bool userTriggered, double timeOffset)
{ {
if (auto && !userTriggered && timeOffset > 0) if (auto && !userTriggered && timeOffset > hitOffset && CheckHittable?.Invoke(this, Time.Current) != false)
{ {
// force success // force success
ApplyResult(r => r.Type = HitResult.Great); ApplyResult(r => r.Type = HitResult.Great);

View File

@ -16,11 +16,11 @@ namespace osu.Game.Rulesets.Osu.Tests
Scheduler.AddDelayed(() => comboIndex.Value++, 250, true); Scheduler.AddDelayed(() => comboIndex.Value++, 250, true);
} }
protected override TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto) protected override TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto, double hitOffset = 0)
{ {
circle.ComboIndexBindable.BindTo(comboIndex); circle.ComboIndexBindable.BindTo(comboIndex);
circle.IndexInCurrentComboBindable.BindTo(comboIndex); circle.IndexInCurrentComboBindable.BindTo(comboIndex);
return base.CreateDrawableHitCircle(circle, auto); return base.CreateDrawableHitCircle(circle, auto, hitOffset);
} }
} }
} }

View File

@ -26,9 +26,9 @@ namespace osu.Game.Rulesets.Osu.Tests
return base.CreateBeatmapForSkinProvider(); return base.CreateBeatmapForSkinProvider();
} }
protected override TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto) protected override TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto, double hitOffset = 0)
{ {
var drawableHitObject = base.CreateDrawableHitCircle(circle, auto); var drawableHitObject = base.CreateDrawableHitCircle(circle, auto, hitOffset);
Debug.Assert(drawableHitObject.HitObject.HitWindows != null); Debug.Assert(drawableHitObject.HitObject.HitWindows != null);

View File

@ -335,8 +335,8 @@ namespace osu.Game.Rulesets.Osu.Tests
var drawable = CreateDrawableSlider(slider); var drawable = CreateDrawableSlider(slider);
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObjects>()) foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObject>())
mod.ApplyToDrawableHitObjects(new[] { drawable }); mod.ApplyToDrawableHitObject(drawable);
drawable.OnNewResult += onNewResult; drawable.OnNewResult += onNewResult;

View File

@ -85,8 +85,8 @@ namespace osu.Game.Rulesets.Osu.Tests
Scale = new Vector2(0.75f) Scale = new Vector2(0.75f)
}; };
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObjects>()) foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObject>())
mod.ApplyToDrawableHitObjects(new[] { drawableSpinner }); mod.ApplyToDrawableHitObject(drawableSpinner);
return drawableSpinner; return drawableSpinner;
} }

View File

@ -0,0 +1,97 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModApproachDifferent : Mod, IApplicableToDrawableHitObject
{
public override string Name => "Approach Different";
public override string Acronym => "AD";
public override string Description => "Never trust the approach circles...";
public override double ScoreMultiplier => 1;
public override IconUsage? Icon { get; } = FontAwesome.Regular.Circle;
[SettingSource("Initial size", "Change the initial size of the approach circle, relative to hit circles.", 0)]
public BindableFloat Scale { get; } = new BindableFloat(4)
{
Precision = 0.1f,
MinValue = 2,
MaxValue = 10,
};
[SettingSource("Style", "Change the animation style of the approach circles.", 1)]
public Bindable<AnimationStyle> Style { get; } = new Bindable<AnimationStyle>();
public void ApplyToDrawableHitObject(DrawableHitObject drawable)
{
drawable.ApplyCustomUpdateState += (drawableObject, state) =>
{
if (!(drawableObject is DrawableHitCircle drawableHitCircle)) return;
var hitCircle = drawableHitCircle.HitObject;
drawableHitCircle.ApproachCircle.ClearTransforms(targetMember: nameof(Scale));
using (drawableHitCircle.BeginAbsoluteSequence(hitCircle.StartTime - hitCircle.TimePreempt))
drawableHitCircle.ApproachCircle.ScaleTo(Scale.Value).ScaleTo(1f, hitCircle.TimePreempt, getEasing(Style.Value));
};
}
private Easing getEasing(AnimationStyle style)
{
switch (style)
{
default:
return Easing.None;
case AnimationStyle.Accelerate1:
return Easing.In;
case AnimationStyle.Accelerate2:
return Easing.InCubic;
case AnimationStyle.Accelerate3:
return Easing.InQuint;
case AnimationStyle.Gravity:
return Easing.InBack;
case AnimationStyle.Decelerate1:
return Easing.Out;
case AnimationStyle.Decelerate2:
return Easing.OutCubic;
case AnimationStyle.Decelerate3:
return Easing.OutQuint;
case AnimationStyle.InOut1:
return Easing.InOutCubic;
case AnimationStyle.InOut2:
return Easing.InOutQuint;
}
}
public enum AnimationStyle
{
Gravity,
InOut1,
InOut2,
Accelerate1,
Accelerate2,
Accelerate3,
Decelerate1,
Decelerate2,
Decelerate3,
}
}
}

View File

@ -1,7 +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.
using System.Collections.Generic;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
@ -9,22 +8,19 @@ using osu.Game.Rulesets.Osu.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Mods namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModBarrelRoll : ModBarrelRoll<OsuHitObject>, IApplicableToDrawableHitObjects public class OsuModBarrelRoll : ModBarrelRoll<OsuHitObject>, IApplicableToDrawableHitObject
{ {
public void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables) public void ApplyToDrawableHitObject(DrawableHitObject d)
{ {
foreach (var d in drawables) d.OnUpdate += _ =>
{ {
d.OnUpdate += _ => switch (d)
{ {
switch (d) case DrawableHitCircle circle:
{ circle.CirclePiece.Rotation = -CurrentRotation;
case DrawableHitCircle circle: break;
circle.CirclePiece.Rotation = -CurrentRotation; }
break; };
}
};
}
} }
} }
} }

View File

@ -1,7 +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.
using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Configuration; using osu.Game.Configuration;
@ -15,7 +14,7 @@ using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.Mods namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModClassic : ModClassic, IApplicableToHitObject, IApplicableToDrawableHitObjects, IApplicableToDrawableRuleset<OsuHitObject> public class OsuModClassic : ModClassic, IApplicableToHitObject, IApplicableToDrawableHitObject, IApplicableToDrawableRuleset<OsuHitObject>
{ {
[SettingSource("No slider head accuracy requirement", "Scores sliders proportionally to the number of ticks hit.")] [SettingSource("No slider head accuracy requirement", "Scores sliders proportionally to the number of ticks hit.")]
public Bindable<bool> NoSliderHeadAccuracy { get; } = new BindableBool(true); public Bindable<bool> NoSliderHeadAccuracy { get; } = new BindableBool(true);
@ -54,24 +53,21 @@ namespace osu.Game.Rulesets.Osu.Mods
osuRuleset.Playfield.HitPolicy = new ObjectOrderedHitPolicy(); osuRuleset.Playfield.HitPolicy = new ObjectOrderedHitPolicy();
} }
public void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables) public void ApplyToDrawableHitObject(DrawableHitObject obj)
{ {
foreach (var obj in drawables) switch (obj)
{ {
switch (obj) case DrawableSlider slider:
{ slider.Ball.InputTracksVisualSize = !FixedFollowCircleHitArea.Value;
case DrawableSlider slider: break;
slider.Ball.InputTracksVisualSize = !FixedFollowCircleHitArea.Value;
break;
case DrawableSliderHead head: case DrawableSliderHead head:
head.TrackFollowCircle = !NoSliderHeadMovement.Value; head.TrackFollowCircle = !NoSliderHeadMovement.Value;
break; break;
case DrawableSliderTail tail: case DrawableSliderTail tail:
tail.SamplePlaysOnlyOnHit = !AlwaysPlayTailSample.Value; tail.SamplePlaysOnlyOnHit = !AlwaysPlayTailSample.Value;
break; break;
}
} }
} }
} }

View File

@ -2,8 +2,6 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Input; using osu.Framework.Input;
@ -19,7 +17,7 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Mods namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModFlashlight : ModFlashlight<OsuHitObject>, IApplicableToDrawableHitObjects public class OsuModFlashlight : ModFlashlight<OsuHitObject>, IApplicableToDrawableHitObject
{ {
public override double ScoreMultiplier => 1.12; public override double ScoreMultiplier => 1.12;
@ -31,12 +29,10 @@ namespace osu.Game.Rulesets.Osu.Mods
public override Flashlight CreateFlashlight() => flashlight = new OsuFlashlight(); public override Flashlight CreateFlashlight() => flashlight = new OsuFlashlight();
public void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables) public void ApplyToDrawableHitObject(DrawableHitObject drawable)
{ {
foreach (var s in drawables.OfType<DrawableSlider>()) if (drawable is DrawableSlider s)
{
s.Tracking.ValueChanged += flashlight.OnSliderTrackingChange; s.Tracking.ValueChanged += flashlight.OnSliderTrackingChange;
}
} }
public override void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset) public override void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Utils; using osu.Framework.Utils;
@ -13,7 +12,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Mods namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModSpunOut : Mod, IApplicableToDrawableHitObjects public class OsuModSpunOut : Mod, IApplicableToDrawableHitObject
{ {
public override string Name => "Spun Out"; public override string Name => "Spun Out";
public override string Acronym => "SO"; public override string Acronym => "SO";
@ -23,15 +22,12 @@ namespace osu.Game.Rulesets.Osu.Mods
public override double ScoreMultiplier => 0.9; public override double ScoreMultiplier => 0.9;
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(OsuModAutopilot) }; public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(OsuModAutopilot) };
public void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables) public void ApplyToDrawableHitObject(DrawableHitObject hitObject)
{ {
foreach (var hitObject in drawables) if (hitObject is DrawableSpinner spinner)
{ {
if (hitObject is DrawableSpinner spinner) spinner.HandleUserInput = false;
{ spinner.OnUpdate += onSpinnerUpdate;
spinner.HandleUserInput = false;
spinner.OnUpdate += onSpinnerUpdate;
}
} }
} }

View File

@ -172,6 +172,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
base.UpdateStartTimeStateTransforms(); base.UpdateStartTimeStateTransforms();
// always fade out at the circle's start time (to match user expectations).
ApproachCircle.FadeOut(50); ApproachCircle.FadeOut(50);
} }
@ -182,6 +183,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
// todo: temporary / arbitrary, used for lifetime optimisation. // todo: temporary / arbitrary, used for lifetime optimisation.
this.Delay(800).FadeOut(); this.Delay(800).FadeOut();
// in the case of an early state change, the fade should be expedited to the current point in time.
if (HitStateUpdateTime < HitObject.StartTime)
ApproachCircle.FadeOut(50);
switch (state) switch (state)
{ {
case ArmedState.Idle: case ArmedState.Idle:

View File

@ -187,6 +187,7 @@ namespace osu.Game.Rulesets.Osu
new MultiMod(new ModWindUp(), new ModWindDown()), new MultiMod(new ModWindUp(), new ModWindDown()),
new OsuModTraceable(), new OsuModTraceable(),
new OsuModBarrelRoll(), new OsuModBarrelRoll(),
new OsuModApproachDifferent(),
}; };
case ModType.System: case ModType.System:

View File

@ -0,0 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Taiko.Tests.Mods
{
public abstract class TaikoModTestScene : ModTestScene
{
protected sealed override Ruleset CreatePlayerRuleset() => new TaikoRuleset();
}
}

View File

@ -0,0 +1,24 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Game.Rulesets.Taiko.Mods;
namespace osu.Game.Rulesets.Taiko.Tests.Mods
{
public class TestSceneTaikoModHidden : TaikoModTestScene
{
[Test]
public void TestDefaultBeatmapTest() => CreateModTest(new ModTestData
{
Mod = new TaikoModHidden(),
Autoplay = true,
PassCondition = checkSomeAutoplayHits
});
private bool checkSomeAutoplayHits()
=> Player.ScoreProcessor.JudgedHits >= 4
&& Player.Results.All(result => result.Type == result.Judgement.MaxResult);
}
}

View File

@ -1,23 +1,93 @@
// 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.
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
namespace osu.Game.Rulesets.Taiko.Mods namespace osu.Game.Rulesets.Taiko.Mods
{ {
public class TaikoModHidden : ModHidden public class TaikoModHidden : ModHidden, IApplicableToDifficulty
{ {
public override string Description => @"Beats fade out before you hit them!"; public override string Description => @"Beats fade out before you hit them!";
public override double ScoreMultiplier => 1.06; public override double ScoreMultiplier => 1.06;
public override bool HasImplementation => false;
/// <summary>
/// In osu-stable, the hit position is 160, so the active playfield is essentially 160 pixels shorter
/// than the actual screen width. The normalized playfield height is 480, so on a 4:3 screen the
/// playfield ratio of the active area up to the hit position will actually be (640 - 160) / 480 = 1.
/// For custom resolutions/aspect ratios (x:y), the screen width given the normalized height becomes 480 * x / y instead,
/// and the playfield ratio becomes (480 * x / y - 160) / 480 = x / y - 1/3.
/// This constant is equal to the playfield ratio on 4:3 screens divided by the playfield ratio on 16:9 screens.
/// </summary>
private const double hd_sv_scale = (4.0 / 3.0 - 1.0 / 3.0) / (16.0 / 9.0 - 1.0 / 3.0);
private double originalSliderMultiplier;
private ControlPointInfo controlPointInfo;
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
{ {
ApplyNormalVisibilityState(hitObject, state);
}
protected double MultiplierAt(double position)
{
double beatLength = controlPointInfo.TimingPointAt(position).BeatLength;
double speedMultiplier = controlPointInfo.DifficultyPointAt(position).SpeedMultiplier;
return originalSliderMultiplier * speedMultiplier * TimingControlPoint.DEFAULT_BEAT_LENGTH / beatLength;
} }
protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state) protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state)
{ {
switch (hitObject)
{
case DrawableDrumRollTick _:
case DrawableHit _:
double preempt = 10000 / MultiplierAt(hitObject.HitObject.StartTime);
double start = hitObject.HitObject.StartTime - preempt * 0.6;
double duration = preempt * 0.3;
using (hitObject.BeginAbsoluteSequence(start))
{
hitObject.FadeOut(duration);
// DrawableHitObject sets LifetimeEnd to LatestTransformEndTime if it isn't manually changed.
// in order for the object to not be killed before its actual end time (as the latest transform ends earlier), set lifetime end explicitly.
hitObject.LifetimeEnd = state == ArmedState.Idle || !hitObject.AllJudged
? hitObject.HitObject.GetEndTime() + hitObject.HitObject.HitWindows.WindowFor(HitResult.Miss)
: hitObject.HitStateUpdateTime;
}
break;
}
}
public void ReadFromDifficulty(BeatmapDifficulty difficulty)
{
}
public void ApplyToDifficulty(BeatmapDifficulty difficulty)
{
// needs to be read after all processing has been run (TaikoBeatmapConverter applies an adjustment which would otherwise be omitted).
originalSliderMultiplier = difficulty.SliderMultiplier;
// osu-stable has an added playfield cover that essentially forces a 4:3 playfield ratio, by cutting off all objects past that size.
// This is not yet implemented; instead a playfield adjustment container is present which maintains a 16:9 ratio.
// For now, increase the slider multiplier proportionally so that the notes stay on the screen for the same amount of time as on stable.
// Note that this means that the notes will scroll faster as they have a longer distance to travel on the screen in that same amount of time.
difficulty.SliderMultiplier /= hd_sv_scale;
}
public override void ApplyToBeatmap(IBeatmap beatmap)
{
controlPointInfo = beatmap.ControlPointInfo;
} }
} }
} }

View File

@ -161,15 +161,18 @@ namespace osu.Game.Tests.Visual.Background
private void loadNextBackground() private void loadNextBackground()
{ {
SeasonalBackground previousBackground = null;
SeasonalBackground background = null; SeasonalBackground background = null;
AddStep("create next background", () => AddStep("create next background", () =>
{ {
previousBackground = (SeasonalBackground)backgroundContainer.SingleOrDefault();
background = backgroundLoader.LoadNextBackground(); background = backgroundLoader.LoadNextBackground();
LoadComponentAsync(background, bg => backgroundContainer.Child = bg); LoadComponentAsync(background, bg => backgroundContainer.Child = bg);
}); });
AddUntilStep("background loaded", () => background.IsLoaded); AddUntilStep("background loaded", () => background.IsLoaded);
AddAssert("background is different", () => !background.Equals(previousBackground));
} }
private void assertAnyBackground() private void assertAnyBackground()

View File

@ -66,12 +66,12 @@ namespace osu.Game.Tests.Visual.Gameplay
} }
[Test] [Test]
public void TestStoryboardExitToSkipOutro() public void TestStoryboardExitDuringOutroStillExits()
{ {
CreateTest(null); CreateTest(null);
AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value); AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value);
AddStep("exit via pause", () => Player.ExitViaPause()); AddStep("exit via pause", () => Player.ExitViaPause());
AddAssert("score shown", () => Player.IsScoreShown); AddAssert("player exited", () => !Player.IsCurrentScreen() && Player.GetChildScreen() == null);
} }
[TestCase(false)] [TestCase(false)]

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using JetBrains.Annotations;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Lists; using osu.Framework.Lists;
@ -66,6 +67,7 @@ namespace osu.Game.Beatmaps.ControlPoints
/// </summary> /// </summary>
/// <param name="time">The time to find the difficulty control point at.</param> /// <param name="time">The time to find the difficulty control point at.</param>
/// <returns>The difficulty control point.</returns> /// <returns>The difficulty control point.</returns>
[NotNull]
public DifficultyControlPoint DifficultyPointAt(double time) => binarySearchWithFallback(DifficultyPoints, time, DifficultyControlPoint.DEFAULT); public DifficultyControlPoint DifficultyPointAt(double time) => binarySearchWithFallback(DifficultyPoints, time, DifficultyControlPoint.DEFAULT);
/// <summary> /// <summary>
@ -73,6 +75,7 @@ namespace osu.Game.Beatmaps.ControlPoints
/// </summary> /// </summary>
/// <param name="time">The time to find the effect control point at.</param> /// <param name="time">The time to find the effect control point at.</param>
/// <returns>The effect control point.</returns> /// <returns>The effect control point.</returns>
[NotNull]
public EffectControlPoint EffectPointAt(double time) => binarySearchWithFallback(EffectPoints, time, EffectControlPoint.DEFAULT); public EffectControlPoint EffectPointAt(double time) => binarySearchWithFallback(EffectPoints, time, EffectControlPoint.DEFAULT);
/// <summary> /// <summary>
@ -80,6 +83,7 @@ namespace osu.Game.Beatmaps.ControlPoints
/// </summary> /// </summary>
/// <param name="time">The time to find the sound control point at.</param> /// <param name="time">The time to find the sound control point at.</param>
/// <returns>The sound control point.</returns> /// <returns>The sound control point.</returns>
[NotNull]
public SampleControlPoint SamplePointAt(double time) => binarySearchWithFallback(SamplePoints, time, SamplePoints.Count > 0 ? SamplePoints[0] : SampleControlPoint.DEFAULT); public SampleControlPoint SamplePointAt(double time) => binarySearchWithFallback(SamplePoints, time, SamplePoints.Count > 0 ? SamplePoints[0] : SampleControlPoint.DEFAULT);
/// <summary> /// <summary>
@ -87,6 +91,7 @@ namespace osu.Game.Beatmaps.ControlPoints
/// </summary> /// </summary>
/// <param name="time">The time to find the timing control point at.</param> /// <param name="time">The time to find the timing control point at.</param>
/// <returns>The timing control point.</returns> /// <returns>The timing control point.</returns>
[NotNull]
public TimingControlPoint TimingPointAt(double time) => binarySearchWithFallback(TimingPoints, time, TimingPoints.Count > 0 ? TimingPoints[0] : TimingControlPoint.DEFAULT); public TimingControlPoint TimingPointAt(double time) => binarySearchWithFallback(TimingPoints, time, TimingPoints.Count > 0 ? TimingPoints[0] : TimingControlPoint.DEFAULT);
/// <summary> /// <summary>

View File

@ -101,10 +101,20 @@ namespace osu.Game.Beatmaps
/// Rulesets ordered descending by their respective recommended difficulties. /// Rulesets ordered descending by their respective recommended difficulties.
/// The currently selected ruleset will always be first. /// The currently selected ruleset will always be first.
/// </returns> /// </returns>
private IEnumerable<RulesetInfo> orderedRulesets => private IEnumerable<RulesetInfo> orderedRulesets
recommendedDifficultyMapping {
.OrderByDescending(pair => pair.Value).Select(pair => pair.Key).Where(r => !r.Equals(ruleset.Value)) get
.Prepend(ruleset.Value); {
if (LoadState < LoadState.Ready || ruleset.Value == null)
return Enumerable.Empty<RulesetInfo>();
return recommendedDifficultyMapping
.OrderByDescending(pair => pair.Value)
.Select(pair => pair.Key)
.Where(r => !r.Equals(ruleset.Value))
.Prepend(ruleset.Value);
}
}
private void onlineStateChanged(ValueChangedEvent<APIState> state) => Schedule(() => private void onlineStateChanged(ValueChangedEvent<APIState> state) => Schedule(() =>
{ {

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.Globalization;
using osu.Game.Localisation;
namespace osu.Game.Extensions
{
/// <summary>
/// Conversion utilities for the <see cref="Language"/> enum.
/// </summary>
public static class LanguageExtensions
{
/// <summary>
/// Returns the culture code of the <see cref="CultureInfo"/> that corresponds to the supplied <paramref name="language"/>.
/// </summary>
/// <remarks>
/// This is required as enum member names are not allowed to contain hyphens.
/// </remarks>
public static string ToCultureCode(this Language language)
=> language.ToString().Replace("_", "-");
/// <summary>
/// Attempts to parse the supplied <paramref name="cultureCode"/> to a <see cref="Language"/> value.
/// </summary>
/// <param name="cultureCode">The code of the culture to parse.</param>
/// <param name="language">The parsed <see cref="Language"/>. Valid only if the return value of the method is <see langword="true" />.</param>
/// <returns>Whether the parsing succeeded.</returns>
public static bool TryParseCultureCode(string cultureCode, out Language language)
=> Enum.TryParse(cultureCode.Replace("-", "_"), out language);
}
}

View File

@ -99,5 +99,14 @@ namespace osu.Game.Graphics.Backgrounds
// ensure we're not loading in without a transition. // ensure we're not loading in without a transition.
this.FadeInFromZero(200, Easing.InOutSine); this.FadeInFromZero(200, Easing.InOutSine);
} }
public override bool Equals(Background other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return other.GetType() == GetType()
&& ((SeasonalBackground)other).url == url;
}
} }
} }

View File

@ -160,7 +160,7 @@ namespace osu.Game.Graphics.UserInterface
Margin = new MarginPadding { Top = 5, Bottom = 5 }, Margin = new MarginPadding { Top = 5, Bottom = 5 },
Origin = Anchor.BottomLeft, Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft, Anchor = Anchor.BottomLeft,
Text = (value as IHasDescription)?.Description ?? (value as Enum)?.GetDescription() ?? value.ToString(), Text = (value as IHasDescription)?.Description ?? (value as Enum)?.GetLocalisableDescription() ?? value.ToString(),
Font = OsuFont.GetFont(size: 14) Font = OsuFont.GetFont(size: 14)
}, },
Bar = new Box Bar = new Box

View File

@ -11,6 +11,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
namespace osu.Game.Graphics.UserInterface namespace osu.Game.Graphics.UserInterface
@ -81,7 +82,7 @@ namespace osu.Game.Graphics.UserInterface
Active.BindValueChanged(active => Text.Font = Text.Font.With(Typeface.Torus, weight: active.NewValue ? FontWeight.Bold : FontWeight.Medium), true); Active.BindValueChanged(active => Text.Font = Text.Font.With(Typeface.Torus, weight: active.NewValue ? FontWeight.Bold : FontWeight.Medium), true);
} }
protected virtual string CreateText() => (Value as Enum)?.GetDescription() ?? Value.ToString(); protected virtual LocalisableString CreateText() => (Value as Enum)?.GetLocalisableDescription() ?? Value.ToString();
protected override bool OnHover(HoverEvent e) protected override bool OnHover(HoverEvent e)
{ {

View File

@ -10,7 +10,104 @@ namespace osu.Game.Localisation
[Description(@"English")] [Description(@"English")]
en, en,
// TODO: Requires Arabic glyphs to be added to resources (and possibly also RTL support).
// [Description(@"اَلْعَرَبِيَّةُ")]
// ar,
// TODO: Some accented glyphs are missing. Revisit when adding Inter.
// [Description(@"Беларуская мова")]
// be,
[Description(@"Български")]
bg,
[Description(@"Česky")]
cs,
[Description(@"Dansk")]
da,
[Description(@"Deutsch")]
de,
// TODO: Some accented glyphs are missing. Revisit when adding Inter.
// [Description(@"Ελληνικά")]
// el,
[Description(@"español")]
es,
[Description(@"Suomi")]
fi,
[Description(@"français")]
fr,
[Description(@"Magyar")]
hu,
[Description(@"Bahasa Indonesia")]
id,
[Description(@"Italiano")]
it,
[Description(@"日本語")] [Description(@"日本語")]
ja ja,
[Description(@"한국어")]
ko,
[Description(@"Nederlands")]
nl,
[Description(@"Norsk")]
no,
[Description(@"polski")]
pl,
[Description(@"Português")]
pt,
[Description(@"Português (Brasil)")]
pt_br,
[Description(@"Română")]
ro,
[Description(@"Русский")]
ru,
[Description(@"Slovenčina")]
sk,
[Description(@"Svenska")]
sv,
[Description(@"ไทย")]
th,
[Description(@"Tagalog")]
tl,
[Description(@"Türkçe")]
tr,
// TODO: Some accented glyphs are missing. Revisit when adding Inter.
// [Description(@"Українська мова")]
// uk,
[Description(@"Tiếng Việt")]
vi,
[Description(@"简体中文")]
zh,
[Description(@"繁體中文(香港)")]
zh_hk,
[Description(@"繁體中文(台灣)")]
zh_tw
} }
} }

View File

@ -6,7 +6,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using JetBrains.Annotations;
using MessagePack; using MessagePack;
using osu.Game.Online.API; using osu.Game.Online.API;
@ -28,11 +27,9 @@ namespace osu.Game.Online.Multiplayer
[Key(3)] [Key(3)]
public string Name { get; set; } = "Unnamed room"; public string Name { get; set; } = "Unnamed room";
[NotNull]
[Key(4)] [Key(4)]
public IEnumerable<APIMod> RequiredMods { get; set; } = Enumerable.Empty<APIMod>(); public IEnumerable<APIMod> RequiredMods { get; set; } = Enumerable.Empty<APIMod>();
[NotNull]
[Key(5)] [Key(5)]
public IEnumerable<APIMod> AllowedMods { get; set; } = Enumerable.Empty<APIMod>(); public IEnumerable<APIMod> AllowedMods { get; set; } = Enumerable.Empty<APIMod>();

View File

@ -6,7 +6,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using JetBrains.Annotations;
using MessagePack; using MessagePack;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Game.Online.API; using osu.Game.Online.API;
@ -35,7 +34,6 @@ namespace osu.Game.Online.Multiplayer
/// Any mods applicable only to the local user. /// Any mods applicable only to the local user.
/// </summary> /// </summary>
[Key(3)] [Key(3)]
[NotNull]
public IEnumerable<APIMod> Mods { get; set; } = Enumerable.Empty<APIMod>(); public IEnumerable<APIMod> Mods { get; set; } = Enumerable.Empty<APIMod>();
[IgnoreMember] [IgnoreMember]

View File

@ -50,8 +50,10 @@ using osu.Game.Updater;
using osu.Game.Utils; using osu.Game.Utils;
using LogLevel = osu.Framework.Logging.LogLevel; using LogLevel = osu.Framework.Logging.LogLevel;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.IO; using osu.Game.IO;
using osu.Game.Localisation; using osu.Game.Localisation;
using osu.Game.Performance;
using osu.Game.Skinning.Editor; using osu.Game.Skinning.Editor;
namespace osu.Game namespace osu.Game
@ -426,9 +428,12 @@ namespace osu.Game
{ {
// The given ScoreInfo may have missing properties if it was retrieved from online data. Re-retrieve it from the database // The given ScoreInfo may have missing properties if it was retrieved from online data. Re-retrieve it from the database
// to ensure all the required data for presenting a replay are present. // to ensure all the required data for presenting a replay are present.
var databasedScoreInfo = score.OnlineScoreID != null ScoreInfo databasedScoreInfo = null;
? ScoreManager.Query(s => s.OnlineScoreID == score.OnlineScoreID)
: ScoreManager.Query(s => s.Hash == score.Hash); if (score.OnlineScoreID != null)
databasedScoreInfo = ScoreManager.Query(s => s.OnlineScoreID == score.OnlineScoreID);
databasedScoreInfo ??= ScoreManager.Query(s => s.Hash == score.Hash);
if (databasedScoreInfo == null) if (databasedScoreInfo == null)
{ {
@ -484,6 +489,8 @@ namespace osu.Game
protected virtual UpdateManager CreateUpdateManager() => new UpdateManager(); protected virtual UpdateManager CreateUpdateManager() => new UpdateManager();
protected virtual HighPerformanceSession CreateHighPerformanceSession() => new HighPerformanceSession();
protected override Container CreateScalingContainer() => new ScalingContainer(ScalingMode.Everything); protected override Container CreateScalingContainer() => new ScalingContainer(ScalingMode.Everything);
#region Beatmap progression #region Beatmap progression
@ -577,7 +584,7 @@ namespace osu.Game
foreach (var language in Enum.GetValues(typeof(Language)).OfType<Language>()) foreach (var language in Enum.GetValues(typeof(Language)).OfType<Language>())
{ {
var cultureCode = language.ToString(); var cultureCode = language.ToCultureCode();
Localisation.AddLanguage(cultureCode, new ResourceManagerLocalisationStore(cultureCode)); Localisation.AddLanguage(cultureCode, new ResourceManagerLocalisationStore(cultureCode));
} }
@ -712,7 +719,6 @@ namespace osu.Game
PostNotification = n => notifications.Post(n), PostNotification = n => notifications.Post(n),
}, Add, true); }, Add, true);
loadComponentSingleFile(difficultyRecommender, Add);
loadComponentSingleFile(stableImportManager, Add); loadComponentSingleFile(stableImportManager, Add);
loadComponentSingleFile(screenshotManager, Add); loadComponentSingleFile(screenshotManager, Add);
@ -753,8 +759,11 @@ namespace osu.Game
loadComponentSingleFile(new AccountCreationOverlay(), topMostOverlayContent.Add, true); loadComponentSingleFile(new AccountCreationOverlay(), topMostOverlayContent.Add, true);
loadComponentSingleFile(new DialogOverlay(), topMostOverlayContent.Add, true); loadComponentSingleFile(new DialogOverlay(), topMostOverlayContent.Add, true);
loadComponentSingleFile(CreateHighPerformanceSession(), Add);
chatOverlay.State.ValueChanged += state => channelManager.HighPollRate.Value = state.NewValue == Visibility.Visible; chatOverlay.State.ValueChanged += state => channelManager.HighPollRate.Value = state.NewValue == Visibility.Visible;
Add(difficultyRecommender);
Add(externalLinkOpener = new ExternalLinkOpener()); Add(externalLinkOpener = new ExternalLinkOpener());
Add(new MusicKeyBindingHandler()); Add(new MusicKeyBindingHandler());

View File

@ -14,6 +14,7 @@ using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Resources.Localisation.Web;
using osuTK.Graphics; using osuTK.Graphics;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Scoring; using osu.Game.Scoring;
@ -126,15 +127,15 @@ namespace osu.Game.Overlays.BeatmapListing
Padding = new MarginPadding { Horizontal = 10 }, Padding = new MarginPadding { Horizontal = 10 },
Children = new Drawable[] Children = new Drawable[]
{ {
generalFilter = new BeatmapSearchMultipleSelectionFilterRow<SearchGeneral>(@"General"), generalFilter = new BeatmapSearchMultipleSelectionFilterRow<SearchGeneral>(BeatmapsStrings.ListingSearchFiltersGeneral),
modeFilter = new BeatmapSearchRulesetFilterRow(), modeFilter = new BeatmapSearchRulesetFilterRow(),
categoryFilter = new BeatmapSearchFilterRow<SearchCategory>(@"Categories"), categoryFilter = new BeatmapSearchFilterRow<SearchCategory>(BeatmapsStrings.ListingSearchFiltersStatus),
genreFilter = new BeatmapSearchFilterRow<SearchGenre>(@"Genre"), genreFilter = new BeatmapSearchFilterRow<SearchGenre>(BeatmapsStrings.ListingSearchFiltersGenre),
languageFilter = new BeatmapSearchFilterRow<SearchLanguage>(@"Language"), languageFilter = new BeatmapSearchFilterRow<SearchLanguage>(BeatmapsStrings.ListingSearchFiltersLanguage),
extraFilter = new BeatmapSearchMultipleSelectionFilterRow<SearchExtra>(@"Extra"), extraFilter = new BeatmapSearchMultipleSelectionFilterRow<SearchExtra>(BeatmapsStrings.ListingSearchFiltersExtra),
ranksFilter = new BeatmapSearchScoreFilterRow(), ranksFilter = new BeatmapSearchScoreFilterRow(),
playedFilter = new BeatmapSearchFilterRow<SearchPlayed>(@"Played"), playedFilter = new BeatmapSearchFilterRow<SearchPlayed>(BeatmapsStrings.ListingSearchFiltersPlayed),
explicitContentFilter = new BeatmapSearchFilterRow<SearchExplicit>(@"Explicit Content"), explicitContentFilter = new BeatmapSearchFilterRow<SearchExplicit>(BeatmapsStrings.ListingSearchFiltersNsfw),
} }
} }
} }
@ -172,7 +173,7 @@ namespace osu.Game.Overlays.BeatmapListing
public BeatmapSearchTextBox() public BeatmapSearchTextBox()
{ {
PlaceholderText = @"type in keywords..."; PlaceholderText = BeatmapsStrings.ListingSearchPrompt;
} }
protected override bool OnKeyDown(KeyDownEvent e) protected override bool OnKeyDown(KeyDownEvent e)

View File

@ -11,8 +11,8 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osuTK; using osuTK;
using Humanizer;
using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Localisation;
namespace osu.Game.Overlays.BeatmapListing namespace osu.Game.Overlays.BeatmapListing
{ {
@ -26,7 +26,7 @@ namespace osu.Game.Overlays.BeatmapListing
set => current.Current = value; set => current.Current = value;
} }
public BeatmapSearchFilterRow(string headerName) public BeatmapSearchFilterRow(LocalisableString header)
{ {
Drawable filter; Drawable filter;
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y;
@ -53,7 +53,7 @@ namespace osu.Game.Overlays.BeatmapListing
Anchor = Anchor.BottomLeft, Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft, Origin = Anchor.BottomLeft,
Font = OsuFont.GetFont(size: 13), Font = OsuFont.GetFont(size: 13),
Text = headerName.Titleize() Text = header
}, },
filter = CreateFilter() filter = CreateFilter()
} }

View File

@ -9,6 +9,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osuTK; using osuTK;
namespace osu.Game.Overlays.BeatmapListing namespace osu.Game.Overlays.BeatmapListing
@ -19,8 +20,8 @@ namespace osu.Game.Overlays.BeatmapListing
private MultipleSelectionFilter filter; private MultipleSelectionFilter filter;
public BeatmapSearchMultipleSelectionFilterRow(string headerName) public BeatmapSearchMultipleSelectionFilterRow(LocalisableString header)
: base(headerName) : base(header)
{ {
Current.BindTo(filter.Current); Current.BindTo(filter.Current);
} }

View File

@ -3,6 +3,7 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets; using osu.Game.Rulesets;
namespace osu.Game.Overlays.BeatmapListing namespace osu.Game.Overlays.BeatmapListing
@ -10,7 +11,7 @@ namespace osu.Game.Overlays.BeatmapListing
public class BeatmapSearchRulesetFilterRow : BeatmapSearchFilterRow<RulesetInfo> public class BeatmapSearchRulesetFilterRow : BeatmapSearchFilterRow<RulesetInfo>
{ {
public BeatmapSearchRulesetFilterRow() public BeatmapSearchRulesetFilterRow()
: base(@"Mode") : base(BeatmapsStrings.ListingSearchFiltersMode)
{ {
} }

View File

@ -4,6 +4,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Scoring; using osu.Game.Scoring;
namespace osu.Game.Overlays.BeatmapListing namespace osu.Game.Overlays.BeatmapListing
@ -11,7 +13,7 @@ namespace osu.Game.Overlays.BeatmapListing
public class BeatmapSearchScoreFilterRow : BeatmapSearchMultipleSelectionFilterRow<ScoreRank> public class BeatmapSearchScoreFilterRow : BeatmapSearchMultipleSelectionFilterRow<ScoreRank>
{ {
public BeatmapSearchScoreFilterRow() public BeatmapSearchScoreFilterRow()
: base(@"Rank Achieved") : base(BeatmapsStrings.ListingSearchFiltersRank)
{ {
} }
@ -31,20 +33,7 @@ namespace osu.Game.Overlays.BeatmapListing
{ {
} }
protected override string LabelFor(ScoreRank value) protected override LocalisableString LabelFor(ScoreRank value) => value.GetLocalisableDescription();
{
switch (value)
{
case ScoreRank.XH:
return @"Silver SS";
case ScoreRank.SH:
return @"Silver S";
default:
return value.GetDescription();
}
}
} }
} }
} }

View File

@ -7,6 +7,7 @@ using osu.Framework.Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
@ -66,7 +67,7 @@ namespace osu.Game.Overlays.BeatmapListing
/// <summary> /// <summary>
/// Returns the label text to be used for the supplied <paramref name="value"/>. /// Returns the label text to be used for the supplied <paramref name="value"/>.
/// </summary> /// </summary>
protected virtual string LabelFor(T value) => (value as Enum)?.GetDescription() ?? value.ToString(); protected virtual LocalisableString LabelFor(T value) => (value as Enum)?.GetLocalisableDescription() ?? value.ToString();
private void updateState() private void updateState()
{ {

View File

@ -1,10 +1,14 @@
// 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.
using System;
using System.ComponentModel; using System.ComponentModel;
using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.BeatmapListing namespace osu.Game.Overlays.BeatmapListing
{ {
[LocalisableEnum(typeof(SearchCategoryEnumLocalisationMapper))]
public enum SearchCategory public enum SearchCategory
{ {
Any, Any,
@ -23,4 +27,43 @@ namespace osu.Game.Overlays.BeatmapListing
[Description("My Maps")] [Description("My Maps")]
Mine, Mine,
} }
public class SearchCategoryEnumLocalisationMapper : EnumLocalisationMapper<SearchCategory>
{
public override LocalisableString Map(SearchCategory value)
{
switch (value)
{
case SearchCategory.Any:
return BeatmapsStrings.StatusAny;
case SearchCategory.Leaderboard:
return BeatmapsStrings.StatusLeaderboard;
case SearchCategory.Ranked:
return BeatmapsStrings.StatusRanked;
case SearchCategory.Qualified:
return BeatmapsStrings.StatusQualified;
case SearchCategory.Loved:
return BeatmapsStrings.StatusLoved;
case SearchCategory.Favourites:
return BeatmapsStrings.StatusFavourites;
case SearchCategory.Pending:
return BeatmapsStrings.StatusPending;
case SearchCategory.Graveyard:
return BeatmapsStrings.StatusGraveyard;
case SearchCategory.Mine:
return BeatmapsStrings.StatusMine;
default:
throw new ArgumentOutOfRangeException(nameof(value), value, null);
}
}
}
} }

View File

@ -1,11 +1,34 @@
// 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.
using System;
using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.BeatmapListing namespace osu.Game.Overlays.BeatmapListing
{ {
[LocalisableEnum(typeof(SearchExplicitEnumLocalisationMapper))]
public enum SearchExplicit public enum SearchExplicit
{ {
Hide, Hide,
Show Show
} }
public class SearchExplicitEnumLocalisationMapper : EnumLocalisationMapper<SearchExplicit>
{
public override LocalisableString Map(SearchExplicit value)
{
switch (value)
{
case SearchExplicit.Hide:
return BeatmapsStrings.NsfwExclude;
case SearchExplicit.Show:
return BeatmapsStrings.NsfwInclude;
default:
throw new ArgumentOutOfRangeException(nameof(value), value, null);
}
}
}
} }

View File

@ -1,10 +1,14 @@
// 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.
using System;
using System.ComponentModel; using System.ComponentModel;
using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.BeatmapListing namespace osu.Game.Overlays.BeatmapListing
{ {
[LocalisableEnum(typeof(SearchExtraEnumLocalisationMapper))]
public enum SearchExtra public enum SearchExtra
{ {
[Description("Has Video")] [Description("Has Video")]
@ -13,4 +17,22 @@ namespace osu.Game.Overlays.BeatmapListing
[Description("Has Storyboard")] [Description("Has Storyboard")]
Storyboard Storyboard
} }
public class SearchExtraEnumLocalisationMapper : EnumLocalisationMapper<SearchExtra>
{
public override LocalisableString Map(SearchExtra value)
{
switch (value)
{
case SearchExtra.Video:
return BeatmapsStrings.ExtraVideo;
case SearchExtra.Storyboard:
return BeatmapsStrings.ExtraStoryboard;
default:
throw new ArgumentOutOfRangeException(nameof(value), value, null);
}
}
}
} }

View File

@ -1,10 +1,14 @@
// 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.
using System;
using System.ComponentModel; using System.ComponentModel;
using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.BeatmapListing namespace osu.Game.Overlays.BeatmapListing
{ {
[LocalisableEnum(typeof(SearchGeneralEnumLocalisationMapper))]
public enum SearchGeneral public enum SearchGeneral
{ {
[Description("Recommended difficulty")] [Description("Recommended difficulty")]
@ -16,4 +20,25 @@ namespace osu.Game.Overlays.BeatmapListing
[Description("Subscribed mappers")] [Description("Subscribed mappers")]
Follows Follows
} }
public class SearchGeneralEnumLocalisationMapper : EnumLocalisationMapper<SearchGeneral>
{
public override LocalisableString Map(SearchGeneral value)
{
switch (value)
{
case SearchGeneral.Recommended:
return BeatmapsStrings.GeneralRecommended;
case SearchGeneral.Converts:
return BeatmapsStrings.GeneralConverts;
case SearchGeneral.Follows:
return BeatmapsStrings.GeneralFollows;
default:
throw new ArgumentOutOfRangeException(nameof(value), value, null);
}
}
}
} }

View File

@ -1,10 +1,14 @@
// 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.
using System;
using System.ComponentModel; using System.ComponentModel;
using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.BeatmapListing namespace osu.Game.Overlays.BeatmapListing
{ {
[LocalisableEnum(typeof(SearchGenreEnumLocalisationMapper))]
public enum SearchGenre public enum SearchGenre
{ {
Any = 0, Any = 0,
@ -26,4 +30,58 @@ namespace osu.Game.Overlays.BeatmapListing
Folk = 13, Folk = 13,
Jazz = 14 Jazz = 14
} }
public class SearchGenreEnumLocalisationMapper : EnumLocalisationMapper<SearchGenre>
{
public override LocalisableString Map(SearchGenre value)
{
switch (value)
{
case SearchGenre.Any:
return BeatmapsStrings.GenreAny;
case SearchGenre.Unspecified:
return BeatmapsStrings.GenreUnspecified;
case SearchGenre.VideoGame:
return BeatmapsStrings.GenreVideoGame;
case SearchGenre.Anime:
return BeatmapsStrings.GenreAnime;
case SearchGenre.Rock:
return BeatmapsStrings.GenreRock;
case SearchGenre.Pop:
return BeatmapsStrings.GenrePop;
case SearchGenre.Other:
return BeatmapsStrings.GenreOther;
case SearchGenre.Novelty:
return BeatmapsStrings.GenreNovelty;
case SearchGenre.HipHop:
return BeatmapsStrings.GenreHipHop;
case SearchGenre.Electronic:
return BeatmapsStrings.GenreElectronic;
case SearchGenre.Metal:
return BeatmapsStrings.GenreMetal;
case SearchGenre.Classical:
return BeatmapsStrings.GenreClassical;
case SearchGenre.Folk:
return BeatmapsStrings.GenreFolk;
case SearchGenre.Jazz:
return BeatmapsStrings.GenreJazz;
default:
throw new ArgumentOutOfRangeException(nameof(value), value, null);
}
}
}
} }

View File

@ -1,10 +1,14 @@
// 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.
using System;
using osu.Framework.Localisation;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.BeatmapListing namespace osu.Game.Overlays.BeatmapListing
{ {
[LocalisableEnum(typeof(SearchLanguageEnumLocalisationMapper))]
[HasOrderedElements] [HasOrderedElements]
public enum SearchLanguage public enum SearchLanguage
{ {
@ -53,4 +57,61 @@ namespace osu.Game.Overlays.BeatmapListing
[Order(13)] [Order(13)]
Other Other
} }
public class SearchLanguageEnumLocalisationMapper : EnumLocalisationMapper<SearchLanguage>
{
public override LocalisableString Map(SearchLanguage value)
{
switch (value)
{
case SearchLanguage.Any:
return BeatmapsStrings.LanguageAny;
case SearchLanguage.Unspecified:
return BeatmapsStrings.LanguageUnspecified;
case SearchLanguage.English:
return BeatmapsStrings.LanguageEnglish;
case SearchLanguage.Japanese:
return BeatmapsStrings.LanguageJapanese;
case SearchLanguage.Chinese:
return BeatmapsStrings.LanguageChinese;
case SearchLanguage.Instrumental:
return BeatmapsStrings.LanguageInstrumental;
case SearchLanguage.Korean:
return BeatmapsStrings.LanguageKorean;
case SearchLanguage.French:
return BeatmapsStrings.LanguageFrench;
case SearchLanguage.German:
return BeatmapsStrings.LanguageGerman;
case SearchLanguage.Swedish:
return BeatmapsStrings.LanguageSwedish;
case SearchLanguage.Spanish:
return BeatmapsStrings.LanguageSpanish;
case SearchLanguage.Italian:
return BeatmapsStrings.LanguageItalian;
case SearchLanguage.Russian:
return BeatmapsStrings.LanguageRussian;
case SearchLanguage.Polish:
return BeatmapsStrings.LanguagePolish;
case SearchLanguage.Other:
return BeatmapsStrings.LanguageOther;
default:
throw new ArgumentOutOfRangeException(nameof(value), value, null);
}
}
}
} }

View File

@ -1,12 +1,38 @@
// 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.
using System;
using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.BeatmapListing namespace osu.Game.Overlays.BeatmapListing
{ {
[LocalisableEnum(typeof(SearchPlayedEnumLocalisationMapper))]
public enum SearchPlayed public enum SearchPlayed
{ {
Any, Any,
Played, Played,
Unplayed Unplayed
} }
public class SearchPlayedEnumLocalisationMapper : EnumLocalisationMapper<SearchPlayed>
{
public override LocalisableString Map(SearchPlayed value)
{
switch (value)
{
case SearchPlayed.Any:
return BeatmapsStrings.PlayedAny;
case SearchPlayed.Played:
return BeatmapsStrings.PlayedPlayed;
case SearchPlayed.Unplayed:
return BeatmapsStrings.PlayedUnplayed;
default:
throw new ArgumentOutOfRangeException(nameof(value), value, null);
}
}
}
} }

View File

@ -1,8 +1,13 @@
// 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.
using System;
using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.BeatmapListing namespace osu.Game.Overlays.BeatmapListing
{ {
[LocalisableEnum(typeof(SortCriteriaLocalisationMapper))]
public enum SortCriteria public enum SortCriteria
{ {
Title, Title,
@ -14,4 +19,40 @@ namespace osu.Game.Overlays.BeatmapListing
Favourites, Favourites,
Relevance Relevance
} }
public class SortCriteriaLocalisationMapper : EnumLocalisationMapper<SortCriteria>
{
public override LocalisableString Map(SortCriteria value)
{
switch (value)
{
case SortCriteria.Title:
return BeatmapsStrings.ListingSearchSortingTitle;
case SortCriteria.Artist:
return BeatmapsStrings.ListingSearchSortingArtist;
case SortCriteria.Difficulty:
return BeatmapsStrings.ListingSearchSortingDifficulty;
case SortCriteria.Ranked:
return BeatmapsStrings.ListingSearchSortingRanked;
case SortCriteria.Rating:
return BeatmapsStrings.ListingSearchSortingRating;
case SortCriteria.Plays:
return BeatmapsStrings.ListingSearchSortingPlays;
case SortCriteria.Favourites:
return BeatmapsStrings.ListingSearchSortingFavourites;
case SortCriteria.Relevance:
return BeatmapsStrings.ListingSearchSortingRelevance;
default:
throw new ArgumentOutOfRangeException(nameof(value), value, null);
}
}
}
} }

View File

@ -18,6 +18,7 @@ using osu.Game.Beatmaps;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Overlays.BeatmapListing; using osu.Game.Overlays.BeatmapListing;
using osu.Game.Overlays.BeatmapListing.Panels; using osu.Game.Overlays.BeatmapListing.Panels;
using osu.Game.Resources.Localisation.Web;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -232,7 +233,7 @@ namespace osu.Game.Overlays
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Text = @"... nope, nothing found.", Text = BeatmapsStrings.ListingSearchNotFoundQuote,
} }
} }
}); });

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -22,8 +23,8 @@ namespace osu.Game.Overlays.BeatmapSet
{ {
private const float height = 50; private const float height = 50;
private readonly UpdateableAvatar avatar; private UpdateableAvatar avatar;
private readonly FillFlowContainer fields; private FillFlowContainer fields;
private BeatmapSetInfo beatmapSet; private BeatmapSetInfo beatmapSet;
@ -35,11 +36,46 @@ namespace osu.Game.Overlays.BeatmapSet
if (value == beatmapSet) return; if (value == beatmapSet) return;
beatmapSet = value; beatmapSet = value;
Scheduler.AddOnce(updateDisplay);
updateDisplay();
} }
} }
[BackgroundDependencyLoader]
private void load()
{
RelativeSizeAxes = Axes.X;
Height = height;
Children = new Drawable[]
{
new Container
{
AutoSizeAxes = Axes.Both,
CornerRadius = 4,
Masking = true,
Child = avatar = new UpdateableAvatar(showGuestOnNull: false)
{
Size = new Vector2(height),
},
EdgeEffect = new EdgeEffectParameters
{
Colour = Color4.Black.Opacity(0.25f),
Type = EdgeEffectType.Shadow,
Radius = 4,
Offset = new Vector2(0f, 1f),
},
},
fields = new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Padding = new MarginPadding { Left = height + 5 },
},
};
Scheduler.AddOnce(updateDisplay);
}
private void updateDisplay() private void updateDisplay()
{ {
avatar.User = BeatmapSet?.Metadata.Author; avatar.User = BeatmapSet?.Metadata.Author;
@ -69,45 +105,6 @@ namespace osu.Game.Overlays.BeatmapSet
} }
} }
public AuthorInfo()
{
RelativeSizeAxes = Axes.X;
Height = height;
Children = new Drawable[]
{
new Container
{
AutoSizeAxes = Axes.Both,
CornerRadius = 4,
Masking = true,
Child = avatar = new UpdateableAvatar
{
ShowGuestOnNull = false,
Size = new Vector2(height),
},
EdgeEffect = new EdgeEffectParameters
{
Colour = Color4.Black.Opacity(0.25f),
Type = EdgeEffectType.Shadow,
Radius = 4,
Offset = new Vector2(0f, 1f),
},
},
fields = new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Padding = new MarginPadding { Left = height + 5 },
},
};
}
private void load()
{
updateDisplay();
}
private class Field : FillFlowContainer private class Field : FillFlowContainer
{ {
public Field(string first, string second, FontUsage secondFont) public Field(string first, string second, FontUsage secondFont)

View File

@ -61,7 +61,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
}, },
} }
}, },
avatar = new UpdateableAvatar avatar = new UpdateableAvatar(showGuestOnNull: false)
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -75,7 +75,6 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
Offset = new Vector2(0, 2), Offset = new Vector2(0, 2),
Radius = 1, Radius = 1,
}, },
ShowGuestOnNull = false,
}, },
new FillFlowContainer new FillFlowContainer
{ {

View File

@ -51,7 +51,7 @@ namespace osu.Game.Overlays.Chat.Tabs
Child = new DelayedLoadWrapper(avatar = new ClickableAvatar(value.Users.First()) Child = new DelayedLoadWrapper(avatar = new ClickableAvatar(value.Users.First())
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
OpenOnClick = { Value = false }, OpenOnClick = false,
}) })
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,

View File

@ -18,6 +18,7 @@ using JetBrains.Annotations;
using System; using System;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays namespace osu.Game.Overlays
{ {
@ -54,7 +55,7 @@ namespace osu.Game.Overlays
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold),
Text = @"Sort by" Text = SortStrings.Default
}, },
CreateControl().With(c => CreateControl().With(c =>
{ {
@ -143,7 +144,7 @@ namespace osu.Game.Overlays
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold),
Text = (value as Enum)?.GetDescription() ?? value.ToString() Text = (value as Enum)?.GetLocalisableDescription() ?? value.ToString()
} }
} }
}); });

View File

@ -58,13 +58,11 @@ namespace osu.Game.Overlays.Profile.Header
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
Children = new Drawable[] Children = new Drawable[]
{ {
avatar = new UpdateableAvatar avatar = new UpdateableAvatar(openOnClick: false, showGuestOnNull: false)
{ {
Size = new Vector2(avatar_size), Size = new Vector2(avatar_size),
Masking = true, Masking = true,
CornerRadius = avatar_size * 0.25f, CornerRadius = avatar_size * 0.25f,
OpenOnClick = { Value = false },
ShowGuestOnNull = false,
}, },
new Container new Container
{ {

View File

@ -1,11 +1,11 @@
// 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.
using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Configuration; using osu.Framework.Configuration;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Extensions;
using osu.Game.Localisation; using osu.Game.Localisation;
namespace osu.Game.Overlays.Settings.Sections.General namespace osu.Game.Overlays.Settings.Sections.General
@ -35,11 +35,11 @@ namespace osu.Game.Overlays.Settings.Sections.General
}, },
}; };
if (!Enum.TryParse<Language>(frameworkLocale.Value, out var locale)) if (!LanguageExtensions.TryParseCultureCode(frameworkLocale.Value, out var locale))
locale = Language.en; locale = Language.en;
languageSelection.Current.Value = locale; languageSelection.Current.Value = locale;
languageSelection.Current.BindValueChanged(val => frameworkLocale.Value = val.NewValue.ToString()); languageSelection.Current.BindValueChanged(val => frameworkLocale.Value = val.NewValue.ToCultureCode());
} }
} }
} }

View File

@ -19,6 +19,8 @@ namespace osu.Game.Overlays.Settings
Margin = new MarginPadding { Top = 5 }; Margin = new MarginPadding { Top = 5 };
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
} }
protected override DropdownMenu CreateMenu() => base.CreateMenu().With(m => m.MaxHeight = 200);
} }
} }
} }

View File

@ -106,7 +106,19 @@ namespace osu.Game.Overlays
public OverlayHeaderTabItem(T value) public OverlayHeaderTabItem(T value)
: base(value) : base(value)
{ {
Text.Text = ((Value as Enum)?.GetDescription() ?? Value.ToString()).ToLower(); if (!(Value is Enum enumValue))
Text.Text = Value.ToString().ToLower();
else
{
var localisableDescription = enumValue.GetLocalisableDescription();
var nonLocalisableDescription = enumValue.GetDescription();
// If localisable == non-localisable, then we must have a basic string, so .ToLower() is used.
Text.Text = localisableDescription.Equals(nonLocalisableDescription)
? nonLocalisableDescription.ToLower()
: localisableDescription;
}
Text.Font = OsuFont.GetFont(size: 14); Text.Font = OsuFont.GetFont(size: 14);
Text.Margin = new MarginPadding { Vertical = 16.5f }; // 15px padding + 1.5px line-height difference compensation Text.Margin = new MarginPadding { Vertical = 16.5f }; // 15px padding + 1.5px line-height difference compensation
Bar.Margin = new MarginPadding { Bottom = bar_height }; Bar.Margin = new MarginPadding { Bottom = bar_height };

View File

@ -32,14 +32,13 @@ namespace osu.Game.Overlays.Toolbar
Add(new OpaqueBackground { Depth = 1 }); Add(new OpaqueBackground { Depth = 1 });
Flow.Add(avatar = new UpdateableAvatar Flow.Add(avatar = new UpdateableAvatar(openOnClick: false)
{ {
Masking = true, Masking = true,
Size = new Vector2(32), Size = new Vector2(32),
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
CornerRadius = 4, CornerRadius = 4,
OpenOnClick = { Value = false },
EdgeEffect = new EdgeEffectParameters EdgeEffect = new EdgeEffectParameters
{ {
Type = EdgeEffectType.Shadow, Type = EdgeEffectType.Shadow,

View File

@ -0,0 +1,47 @@
// 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.Runtime;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
namespace osu.Game.Performance
{
public class HighPerformanceSession : Component
{
private readonly IBindable<bool> localUserPlaying = new Bindable<bool>();
private GCLatencyMode originalGCMode;
[BackgroundDependencyLoader]
private void load(OsuGame game)
{
localUserPlaying.BindTo(game.LocalUserPlaying);
}
protected override void LoadComplete()
{
base.LoadComplete();
localUserPlaying.BindValueChanged(playing =>
{
if (playing.NewValue)
EnableHighPerformanceSession();
else
DisableHighPerformanceSession();
}, true);
}
protected virtual void EnableHighPerformanceSession()
{
originalGCMode = GCSettings.LatencyMode;
GCSettings.LatencyMode = GCLatencyMode.LowLatency;
}
protected virtual void DisableHighPerformanceSession()
{
if (GCSettings.LatencyMode == GCLatencyMode.LowLatency)
GCSettings.LatencyMode = originalGCMode;
}
}
}

View File

@ -43,6 +43,9 @@ namespace osu.Game.Rulesets.Edit
protected readonly Ruleset Ruleset; protected readonly Ruleset Ruleset;
// Provides `Playfield`
private DependencyContainer dependencies;
[Resolved] [Resolved]
protected EditorClock EditorClock { get; private set; } protected EditorClock EditorClock { get; private set; }
@ -69,6 +72,9 @@ namespace osu.Game.Rulesets.Edit
Ruleset = ruleset; Ruleset = ruleset;
} }
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) =>
dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
@ -88,6 +94,8 @@ namespace osu.Game.Rulesets.Edit
return; return;
} }
dependencies.CacheAs(Playfield);
const float toolbar_width = 200; const float toolbar_width = 200;
InternalChildren = new Drawable[] InternalChildren = new Drawable[]

View File

@ -1,7 +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.
using System.Collections.Generic;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Mods namespace osu.Game.Rulesets.Mods
@ -9,13 +8,12 @@ namespace osu.Game.Rulesets.Mods
/// <summary> /// <summary>
/// An interface for <see cref="Mod"/>s that can be applied to <see cref="DrawableHitObject"/>s. /// An interface for <see cref="Mod"/>s that can be applied to <see cref="DrawableHitObject"/>s.
/// </summary> /// </summary>
public interface IApplicableToDrawableHitObjects : IApplicableMod public interface IApplicableToDrawableHitObject : IApplicableMod
{ {
/// <summary> /// <summary>
/// Applies this <see cref="IApplicableToDrawableHitObjects"/> to a list of <see cref="DrawableHitObject"/>s. /// Applies this <see cref="IApplicableToDrawableHitObject"/> to a <see cref="DrawableHitObject"/>.
/// This will only be invoked with top-level <see cref="DrawableHitObject"/>s. Access <see cref="DrawableHitObject.NestedHitObjects"/> if adjusting nested objects is necessary. /// This will only be invoked with top-level <see cref="DrawableHitObject"/>s. Access <see cref="DrawableHitObject.NestedHitObjects"/> if adjusting nested objects is necessary.
/// </summary> /// </summary>
/// <param name="drawables">The list of <see cref="DrawableHitObject"/>s to apply to.</param> void ApplyToDrawableHitObject(DrawableHitObject drawable);
void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables);
} }
} }

View File

@ -0,0 +1,18 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Mods
{
[Obsolete(@"Use the singular version IApplicableToDrawableHitObject instead.")] // Can be removed 20211216
public interface IApplicableToDrawableHitObjects : IApplicableToDrawableHitObject
{
void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables);
void IApplicableToDrawableHitObject.ApplyToDrawableHitObject(DrawableHitObject drawable) => ApplyToDrawableHitObjects(drawable.Yield());
}
}

View File

@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Mods
/// A <see cref="Mod"/> which applies visibility adjustments to <see cref="DrawableHitObject"/>s /// A <see cref="Mod"/> which applies visibility adjustments to <see cref="DrawableHitObject"/>s
/// with an optional increased visibility adjustment depending on the user's "increase first object visibility" setting. /// with an optional increased visibility adjustment depending on the user's "increase first object visibility" setting.
/// </summary> /// </summary>
public abstract class ModWithVisibilityAdjustment : Mod, IReadFromConfig, IApplicableToBeatmap, IApplicableToDrawableHitObjects public abstract class ModWithVisibilityAdjustment : Mod, IReadFromConfig, IApplicableToBeatmap, IApplicableToDrawableHitObject
{ {
/// <summary> /// <summary>
/// The first adjustable object. /// The first adjustable object.
@ -73,19 +73,16 @@ namespace osu.Game.Rulesets.Mods
} }
} }
public virtual void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables) public virtual void ApplyToDrawableHitObject(DrawableHitObject dho)
{ {
foreach (var dho in drawables) dho.ApplyCustomUpdateState += (o, state) =>
{ {
dho.ApplyCustomUpdateState += (o, state) => // Increased visibility is applied to the entire first object, including all of its nested hitobjects.
{ if (IncreaseFirstObjectVisibility.Value && isObjectEqualToOrNestedIn(o.HitObject, FirstObject))
// Increased visibility is applied to the entire first object, including all of its nested hitobjects. ApplyIncreasedVisibilityState(o, state);
if (IncreaseFirstObjectVisibility.Value && isObjectEqualToOrNestedIn(o.HitObject, FirstObject)) else
ApplyIncreasedVisibilityState(o, state); ApplyNormalVisibilityState(o, state);
else };
ApplyNormalVisibilityState(o, state);
};
}
} }
/// <summary> /// <summary>

View File

@ -6,7 +6,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using JetBrains.Annotations;
using osu.Game.Input.Handlers; using osu.Game.Input.Handlers;
using osu.Game.Replays; using osu.Game.Replays;
@ -117,7 +116,7 @@ namespace osu.Game.Rulesets.Replays
} }
} }
protected virtual bool IsImportant([NotNull] TFrame frame) => false; protected virtual bool IsImportant(TFrame frame) => false;
/// <summary> /// <summary>
/// Update the current frame based on an incoming time value. /// Update the current frame based on an incoming time value.

View File

@ -199,8 +199,11 @@ namespace osu.Game.Rulesets.UI
Playfield.PostProcess(); Playfield.PostProcess();
foreach (var mod in Mods.OfType<IApplicableToDrawableHitObjects>()) foreach (var mod in Mods.OfType<IApplicableToDrawableHitObject>())
mod.ApplyToDrawableHitObjects(Playfield.AllHitObjects); {
foreach (var drawableHitObject in Playfield.AllHitObjects)
mod.ApplyToDrawableHitObject(drawableHitObject);
}
} }
public override void RequestResume(Action continueResume) public override void RequestResume(Action continueResume)

View File

@ -356,8 +356,8 @@ namespace osu.Game.Rulesets.UI
// This is done before Apply() so that the state is updated once when the hitobject is applied. // This is done before Apply() so that the state is updated once when the hitobject is applied.
if (mods != null) if (mods != null)
{ {
foreach (var m in mods.OfType<IApplicableToDrawableHitObjects>()) foreach (var m in mods.OfType<IApplicableToDrawableHitObject>())
m.ApplyToDrawableHitObjects(dho.Yield()); m.ApplyToDrawableHitObject(dho);
} }
} }

View File

@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
/// </remarks> /// </remarks>
public double TimeAtPosition(float localPosition, double currentTime) public double TimeAtPosition(float localPosition, double currentTime)
{ {
float scrollPosition = axisInverted ? scrollLength - localPosition : localPosition; float scrollPosition = axisInverted ? -localPosition : localPosition;
return scrollingInfo.Algorithm.TimeAt(scrollPosition, currentTime, timeRange.Value, scrollLength); return scrollingInfo.Algorithm.TimeAt(scrollPosition, currentTime, timeRange.Value, scrollLength);
} }
@ -81,8 +81,10 @@ namespace osu.Game.Rulesets.UI.Scrolling
/// </remarks> /// </remarks>
public double TimeAtScreenSpacePosition(Vector2 screenSpacePosition) public double TimeAtScreenSpacePosition(Vector2 screenSpacePosition)
{ {
Vector2 localPosition = ToLocalSpace(screenSpacePosition); Vector2 pos = ToLocalSpace(screenSpacePosition);
return TimeAtPosition(scrollingAxis == Direction.Horizontal ? localPosition.X : localPosition.Y, Time.Current); float localPosition = scrollingAxis == Direction.Horizontal ? pos.X : pos.Y;
localPosition -= axisInverted ? scrollLength : 0;
return TimeAtPosition(localPosition, Time.Current);
} }
/// <summary> /// <summary>
@ -91,7 +93,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
public float PositionAtTime(double time, double currentTime) public float PositionAtTime(double time, double currentTime)
{ {
float scrollPosition = scrollingInfo.Algorithm.PositionAt(time, currentTime, timeRange.Value, scrollLength); float scrollPosition = scrollingInfo.Algorithm.PositionAt(time, currentTime, timeRange.Value, scrollLength);
return axisInverted ? scrollLength - scrollPosition : scrollPosition; return axisInverted ? -scrollPosition : scrollPosition;
} }
/// <summary> /// <summary>
@ -106,6 +108,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
public Vector2 ScreenSpacePositionAtTime(double time) public Vector2 ScreenSpacePositionAtTime(double time)
{ {
float localPosition = PositionAtTime(time, Time.Current); float localPosition = PositionAtTime(time, Time.Current);
localPosition += axisInverted ? scrollLength : 0;
return scrollingAxis == Direction.Horizontal return scrollingAxis == Direction.Horizontal
? ToScreenSpace(new Vector2(localPosition, DrawHeight / 2)) ? ToScreenSpace(new Vector2(localPosition, DrawHeight / 2))
: ToScreenSpace(new Vector2(DrawWidth / 2, localPosition)); : ToScreenSpace(new Vector2(DrawWidth / 2, localPosition));
@ -236,14 +239,10 @@ namespace osu.Game.Rulesets.UI.Scrolling
{ {
float position = PositionAtTime(hitObject.HitObject.StartTime, currentTime); float position = PositionAtTime(hitObject.HitObject.StartTime, currentTime);
// The position returned from `PositionAtTime` is assuming the `TopLeft` anchor.
// A correction is needed because the hit objects are using a different anchor for each direction (e.g. `BottomCentre` for `Bottom` direction).
float anchorCorrection = axisInverted ? scrollLength : 0;
if (scrollingAxis == Direction.Horizontal) if (scrollingAxis == Direction.Horizontal)
hitObject.X = position - anchorCorrection; hitObject.X = position;
else else
hitObject.Y = position - anchorCorrection; hitObject.Y = position;
} }
} }
} }

View File

@ -1,10 +1,14 @@
// 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.
using System;
using System.ComponentModel; using System.ComponentModel;
using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Scoring namespace osu.Game.Scoring
{ {
[LocalisableEnum(typeof(ScoreRankEnumLocalisationMapper))]
public enum ScoreRank public enum ScoreRank
{ {
[Description(@"D")] [Description(@"D")]
@ -31,4 +35,40 @@ namespace osu.Game.Scoring
[Description(@"SS+")] [Description(@"SS+")]
XH, XH,
} }
public class ScoreRankEnumLocalisationMapper : EnumLocalisationMapper<ScoreRank>
{
public override LocalisableString Map(ScoreRank value)
{
switch (value)
{
case ScoreRank.XH:
return BeatmapsStrings.RankXH;
case ScoreRank.X:
return BeatmapsStrings.RankX;
case ScoreRank.SH:
return BeatmapsStrings.RankSH;
case ScoreRank.S:
return BeatmapsStrings.RankS;
case ScoreRank.A:
return BeatmapsStrings.RankA;
case ScoreRank.B:
return BeatmapsStrings.RankB;
case ScoreRank.C:
return BeatmapsStrings.RankC;
case ScoreRank.D:
return BeatmapsStrings.RankD;
default:
throw new ArgumentOutOfRangeException(nameof(value), value, null);
}
}
}
} }

View File

@ -5,7 +5,6 @@ using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Users; using osu.Game.Users;
@ -91,7 +90,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
}); });
} }
private class UserTile : CompositeDrawable, IHasTooltip private class UserTile : CompositeDrawable
{ {
public User User public User User
{ {
@ -99,8 +98,6 @@ namespace osu.Game.Screens.OnlinePlay.Components
set => avatar.User = value; set => avatar.User = value;
} }
public string TooltipText => User?.Username ?? string.Empty;
private readonly UpdateableAvatar avatar; private readonly UpdateableAvatar avatar;
public UserTile() public UserTile()
@ -116,7 +113,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex(@"27252d"), Colour = Color4Extensions.FromHex(@"27252d"),
}, },
avatar = new UpdateableAvatar { RelativeSizeAxes = Axes.Both }, avatar = new UpdateableAvatar(showUsernameTooltip: true) { RelativeSizeAxes = Axes.Both },
}; };
} }
} }

View File

@ -54,9 +54,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
return new PlaylistsResultsScreen(score, RoomId.Value.Value, PlaylistItem, true); return new PlaylistsResultsScreen(score, RoomId.Value.Value, PlaylistItem, true);
} }
protected override void PrepareScoreForResults() protected override void PrepareScoreForResults(Score score)
{ {
base.PrepareScoreForResults(); base.PrepareScoreForResults(score);
Score.ScoreInfo.TotalScore = (int)Math.Round(ScoreProcessor.GetStandardisedScore()); Score.ScoreInfo.TotalScore = (int)Math.Round(ScoreProcessor.GetStandardisedScore());
} }

View File

@ -181,12 +181,6 @@ namespace osu.Game.Screens.Play
DrawableRuleset.SetRecordTarget(Score); DrawableRuleset.SetRecordTarget(Score);
} }
protected virtual void PrepareScoreForResults()
{
// perform one final population to ensure everything is up-to-date.
ScoreProcessor.PopulateScore(Score.ScoreInfo);
}
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load(AudioManager audio, OsuConfigManager config, OsuGameBase game) private void load(AudioManager audio, OsuConfigManager config, OsuGameBase game)
{ {
@ -301,7 +295,7 @@ namespace osu.Game.Screens.Play
DimmableStoryboard.HasStoryboardEnded.ValueChanged += storyboardEnded => DimmableStoryboard.HasStoryboardEnded.ValueChanged += storyboardEnded =>
{ {
if (storyboardEnded.NewValue && completionProgressDelegate == null) if (storyboardEnded.NewValue && resultsDisplayDelegate == null)
updateCompletionState(); updateCompletionState();
}; };
@ -512,19 +506,25 @@ namespace osu.Game.Screens.Play
} }
/// <summary> /// <summary>
/// Exits the <see cref="Player"/>. /// Attempts to complete a user request to exit gameplay.
/// </summary> /// </summary>
/// <remarks>
/// <list type="bullet">
/// <item>This should only be called in response to a user interaction. Exiting is not guaranteed.</item>
/// <item>This will interrupt any pending progression to the results screen, even if the transition has begun.</item>
/// </list>
/// </remarks>
/// <param name="showDialogFirst"> /// <param name="showDialogFirst">
/// Whether the pause or fail dialog should be shown before performing an exit. /// Whether the pause or fail dialog should be shown before performing an exit.
/// If true and a dialog is not yet displayed, the exit will be blocked the the relevant dialog will display instead. /// If <see langword="true"/> and a dialog is not yet displayed, the exit will be blocked and the relevant dialog will display instead.
/// </param> /// </param>
protected void PerformExit(bool showDialogFirst) protected void PerformExit(bool showDialogFirst)
{ {
// if a restart has been requested, cancel any pending completion (user has shown intent to restart). // if an exit has been requested, cancel any pending completion (the user has shown intention to exit).
completionProgressDelegate?.Cancel(); resultsDisplayDelegate?.Cancel();
// there is a chance that the exit was performed after the transition to results has started. // there is a chance that an exit request occurs after the transition to results has already started.
// we want to give the user what they want, so forcefully return to this screen (to proceed with the upwards exit process). // even in such a case, the user has shown intent, so forcefully return to this screen (to proceed with the upwards exit process).
if (!this.IsCurrentScreen()) if (!this.IsCurrentScreen())
{ {
ValidForResume = false; ValidForResume = false;
@ -547,7 +547,7 @@ namespace osu.Game.Screens.Play
return; return;
} }
// there's a chance the pausing is not supported in the current state, at which point immediate exit should be preferred. // even if this call has requested a dialog, there is a chance the current player mode doesn't support pausing.
if (pausingSupportedByCurrentState) if (pausingSupportedByCurrentState)
{ {
// in the case a dialog needs to be shown, attempt to pause and show it. // in the case a dialog needs to be shown, attempt to pause and show it.
@ -555,14 +555,12 @@ namespace osu.Game.Screens.Play
Pause(); Pause();
return; return;
} }
// if the score is ready for display but results screen has not been pushed yet (e.g. storyboard is still playing beyond gameplay), then transition to results screen instead of exiting.
if (prepareScoreForDisplayTask != null && completionProgressDelegate == null)
{
updateCompletionState(true);
}
} }
// The actual exit is performed if
// - the pause / fail dialog was not requested
// - the pause / fail dialog was requested but is already displayed (user showing intention to exit).
// - the pause / fail dialog was requested but couldn't be displayed due to the type or state of this Player instance.
this.Exit(); this.Exit();
} }
@ -626,7 +624,20 @@ namespace osu.Game.Screens.Play
PerformExit(false); PerformExit(false);
} }
private ScheduledDelegate completionProgressDelegate; /// <summary>
/// This delegate, when set, means the results screen has been queued to appear.
/// The display of the results screen may be delayed by any work being done in <see cref="PrepareScoreForResults"/> and <see cref="PrepareScoreForResultsAsync"/>.
/// </summary>
/// <remarks>
/// Once set, this can *only* be cancelled by rewinding, ie. if <see cref="JudgementProcessor.HasCompleted">ScoreProcessor.HasCompleted</see> becomes <see langword="false"/>.
/// Even if the user requests an exit, it will forcefully proceed to the results screen (see special case in <see cref="OnExiting"/>).
/// </remarks>
private ScheduledDelegate resultsDisplayDelegate;
/// <summary>
/// A task which asynchronously prepares a completed score for display at results.
/// This may include performing net requests or importing the score into the database, generally to ensure things are in a sane state for the play session.
/// </summary>
private Task<ScoreInfo> prepareScoreForDisplayTask; private Task<ScoreInfo> prepareScoreForDisplayTask;
/// <summary> /// <summary>
@ -636,57 +647,44 @@ namespace osu.Game.Screens.Play
/// <exception cref="InvalidOperationException">Thrown if this method is called more than once without changing state.</exception> /// <exception cref="InvalidOperationException">Thrown if this method is called more than once without changing state.</exception>
private void updateCompletionState(bool skipStoryboardOutro = false) private void updateCompletionState(bool skipStoryboardOutro = false)
{ {
// screen may be in the exiting transition phase. // If this player instance is in the middle of an exit, don't attempt any kind of state update.
if (!this.IsCurrentScreen()) if (!this.IsCurrentScreen())
return; return;
// Special case to handle rewinding post-completion. This is the only way already queued forward progress can be cancelled.
// TODO: Investigate whether this can be moved to a RewindablePlayer subclass or similar.
// Currently, even if this scenario is hit, prepareScoreForDisplay has already been queued (and potentially run).
// In scenarios where rewinding is possible (replay, spectating) this is a non-issue as no submission/import work is done,
// but it still doesn't feel right that this exists here.
if (!ScoreProcessor.HasCompleted.Value) if (!ScoreProcessor.HasCompleted.Value)
{ {
completionProgressDelegate?.Cancel(); resultsDisplayDelegate?.Cancel();
completionProgressDelegate = null; resultsDisplayDelegate = null;
ValidForResume = true; ValidForResume = true;
skipOutroOverlay.Hide(); skipOutroOverlay.Hide();
return; return;
} }
if (completionProgressDelegate != null) if (resultsDisplayDelegate != null)
throw new InvalidOperationException($"{nameof(updateCompletionState)} was fired more than once"); throw new InvalidOperationException(@$"{nameof(updateCompletionState)} should never be fired more than once.");
// Only show the completion screen if the player hasn't failed // Only show the completion screen if the player hasn't failed
if (HealthProcessor.HasFailed) if (HealthProcessor.HasFailed)
return; return;
// Setting this early in the process means that even if something were to go wrong in the order of events following, there
// is no chance that a user could return to the (already completed) Player instance from a child screen.
ValidForResume = false; ValidForResume = false;
// ensure we are not writing to the replay any more, as we are about to consume and store the score. // Ensure we are not writing to the replay any more, as we are about to consume and store the score.
DrawableRuleset.SetRecordTarget(null); DrawableRuleset.SetRecordTarget(null);
if (!Configuration.ShowResults) return; if (!Configuration.ShowResults)
return;
prepareScoreForDisplayTask ??= Task.Run(async () => // Asynchronously run score preparation operations (database import, online submission etc.).
{ prepareScoreForDisplayTask ??= Task.Run(prepareScoreForResults);
PrepareScoreForResults();
try
{
await PrepareScoreForResultsAsync(Score).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.Error(ex, "Score preparation failed!");
}
try
{
await ImportScore(Score).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.Error(ex, "Score import failed!");
}
return Score.ScoreInfo;
});
if (skipStoryboardOutro) if (skipStoryboardOutro)
{ {
@ -706,7 +704,33 @@ namespace osu.Game.Screens.Play
scheduleCompletion(); scheduleCompletion();
} }
private void scheduleCompletion() => completionProgressDelegate = Schedule(() => private async Task<ScoreInfo> prepareScoreForResults()
{
// ReSharper disable once MethodHasAsyncOverload
PrepareScoreForResults(Score);
try
{
await PrepareScoreForResultsAsync(Score).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.Error(ex, @"Score preparation failed!");
}
try
{
await ImportScore(Score).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.Error(ex, @"Score import failed!");
}
return Score.ScoreInfo;
}
private void scheduleCompletion() => resultsDisplayDelegate = Schedule(() =>
{ {
if (!prepareScoreForDisplayTask.IsCompleted) if (!prepareScoreForDisplayTask.IsCompleted)
{ {
@ -915,10 +939,11 @@ namespace osu.Game.Screens.Play
{ {
screenSuspension?.Expire(); screenSuspension?.Expire();
if (completionProgressDelegate != null && !completionProgressDelegate.Cancelled && !completionProgressDelegate.Completed) // if the results screen is prepared to be displayed, forcefully show it on an exit request.
// usually if a user has completed a play session they do want to see results. and if they don't they can hit the same key a second time.
if (resultsDisplayDelegate != null && !resultsDisplayDelegate.Cancelled && !resultsDisplayDelegate.Completed)
{ {
// proceed to result screen if beatmap already finished playing resultsDisplayDelegate.RunTask();
completionProgressDelegate.RunTask();
return true; return true;
} }
@ -979,6 +1004,19 @@ namespace osu.Game.Screens.Play
score.ScoreInfo.OnlineScoreID = onlineScoreId; score.ScoreInfo.OnlineScoreID = onlineScoreId;
} }
/// <summary>
/// Prepare the <see cref="Scoring.Score"/> for display at results.
/// </summary>
/// <remarks>
/// This is run synchronously before <see cref="PrepareScoreForResultsAsync"/> is run.
/// </remarks>
/// <param name="score">The <see cref="Scoring.Score"/> to prepare.</param>
protected virtual void PrepareScoreForResults(Score score)
{
// perform one final population to ensure everything is up-to-date.
ScoreProcessor.PopulateScore(score.ScoreInfo);
}
/// <summary> /// <summary>
/// Prepare the <see cref="Scoring.Score"/> for display at results. /// Prepare the <see cref="Scoring.Score"/> for display at results.
/// </summary> /// </summary>

View File

@ -18,6 +18,8 @@ namespace osu.Game.Skinning
private readonly BindableList<ISkinnableDrawable> components = new BindableList<ISkinnableDrawable>(); private readonly BindableList<ISkinnableDrawable> components = new BindableList<ISkinnableDrawable>();
public bool ComponentsLoaded { get; private set; }
public SkinnableTargetContainer(SkinnableTarget target) public SkinnableTargetContainer(SkinnableTarget target)
{ {
Target = target; Target = target;
@ -30,6 +32,7 @@ namespace osu.Game.Skinning
{ {
ClearInternal(); ClearInternal();
components.Clear(); components.Clear();
ComponentsLoaded = false;
content = CurrentSkin.GetDrawableComponent(new SkinnableTargetComponent(Target)) as SkinnableTargetComponentsContainer; content = CurrentSkin.GetDrawableComponent(new SkinnableTargetComponent(Target)) as SkinnableTargetComponentsContainer;
@ -39,8 +42,11 @@ namespace osu.Game.Skinning
{ {
AddInternal(wrapper); AddInternal(wrapper);
components.AddRange(wrapper.Children.OfType<ISkinnableDrawable>()); components.AddRange(wrapper.Children.OfType<ISkinnableDrawable>());
ComponentsLoaded = true;
}); });
} }
else
ComponentsLoaded = true;
} }
/// <inheritdoc cref="ISkinnableTarget"/> /// <inheritdoc cref="ISkinnableTarget"/>

View File

@ -1,6 +1,7 @@
// 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.
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
@ -47,6 +48,8 @@ namespace osu.Game.Tests.Visual
LegacySkin.ResetDrawableTarget(t); LegacySkin.ResetDrawableTarget(t);
t.Reload(); t.Reload();
})); }));
AddUntilStep("wait for components to load", () => this.ChildrenOfType<SkinnableTargetContainer>().All(t => t.ComponentsLoaded));
} }
public class SkinProvidingPlayer : TestPlayer public class SkinProvidingPlayer : TestPlayer

View File

@ -350,7 +350,7 @@ namespace osu.Game.Tests.Visual
if (CurrentTime >= Length) if (CurrentTime >= Length)
{ {
Stop(); Stop();
RaiseCompleted(); // `RaiseCompleted` is not called here to prevent transitioning to the next song.
} }
} }
} }

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
@ -13,16 +12,32 @@ namespace osu.Game.Users.Drawables
{ {
public class ClickableAvatar : Container public class ClickableAvatar : Container
{ {
private const string default_tooltip_text = "view profile";
/// <summary> /// <summary>
/// Whether to open the user's profile when clicked. /// Whether to open the user's profile when clicked.
/// </summary> /// </summary>
public readonly BindableBool OpenOnClick = new BindableBool(true); public bool OpenOnClick
{
set => clickableArea.Enabled.Value = value;
}
/// <summary>
/// By default, the tooltip will show "view profile" as avatars are usually displayed next to a username.
/// Setting this to <c>true</c> exposes the username via tooltip for special cases where this is not true.
/// </summary>
public bool ShowUsernameTooltip
{
set => clickableArea.TooltipText = value ? (user?.Username ?? string.Empty) : default_tooltip_text;
}
private readonly User user; private readonly User user;
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private OsuGame game { get; set; } private OsuGame game { get; set; }
private readonly ClickableArea clickableArea;
/// <summary> /// <summary>
/// A clickable avatar for the specified user, with UI sounds included. /// A clickable avatar for the specified user, with UI sounds included.
/// If <see cref="OpenOnClick"/> is <c>true</c>, clicking will open the user's profile. /// If <see cref="OpenOnClick"/> is <c>true</c>, clicking will open the user's profile.
@ -31,35 +46,35 @@ namespace osu.Game.Users.Drawables
public ClickableAvatar(User user = null) public ClickableAvatar(User user = null)
{ {
this.user = user; this.user = user;
}
[BackgroundDependencyLoader]
private void load(LargeTextureStore textures)
{
ClickableArea clickableArea;
Add(clickableArea = new ClickableArea Add(clickableArea = new ClickableArea
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Action = openProfile Action = openProfile
}); });
}
[BackgroundDependencyLoader]
private void load(LargeTextureStore textures)
{
LoadComponentAsync(new DrawableAvatar(user), clickableArea.Add); LoadComponentAsync(new DrawableAvatar(user), clickableArea.Add);
clickableArea.Enabled.BindTo(OpenOnClick);
} }
private void openProfile() private void openProfile()
{ {
if (!OpenOnClick.Value)
return;
if (user?.Id > 1) if (user?.Id > 1)
game?.ShowUser(user.Id); game?.ShowUser(user.Id);
} }
private class ClickableArea : OsuClickableContainer private class ClickableArea : OsuClickableContainer
{ {
public override string TooltipText => Enabled.Value ? @"view profile" : null; private string tooltip = default_tooltip_text;
public override string TooltipText
{
get => Enabled.Value ? tooltip : null;
set => tooltip = value;
}
protected override bool OnClick(ClickEvent e) protected override bool OnClick(ClickEvent e)
{ {

View File

@ -1,7 +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.
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Effects;
@ -45,33 +44,38 @@ namespace osu.Game.Users.Drawables
protected override double LoadDelay => 200; protected override double LoadDelay => 200;
/// <summary> private readonly bool openOnClick;
/// Whether to show a default guest representation on null user (as opposed to nothing). private readonly bool showUsernameTooltip;
/// </summary> private readonly bool showGuestOnNull;
public bool ShowGuestOnNull = true;
/// <summary> /// <summary>
/// Whether to open the user's profile when clicked. /// Construct a new UpdateableAvatar.
/// </summary> /// </summary>
public readonly BindableBool OpenOnClick = new BindableBool(true); /// <param name="user">The initial user to display.</param>
/// <param name="openOnClick">Whether to open the user's profile when clicked.</param>
public UpdateableAvatar(User user = null) /// <param name="showUsernameTooltip">Whether to show the username rather than "view profile" on the tooltip.</param>
/// <param name="showGuestOnNull">Whether to show a default guest representation on null user (as opposed to nothing).</param>
public UpdateableAvatar(User user = null, bool openOnClick = true, bool showUsernameTooltip = false, bool showGuestOnNull = true)
{ {
this.openOnClick = openOnClick;
this.showUsernameTooltip = showUsernameTooltip;
this.showGuestOnNull = showGuestOnNull;
User = user; User = user;
} }
protected override Drawable CreateDrawable(User user) protected override Drawable CreateDrawable(User user)
{ {
if (user == null && !ShowGuestOnNull) if (user == null && !showGuestOnNull)
return null; return null;
var avatar = new ClickableAvatar(user) var avatar = new ClickableAvatar(user)
{ {
OpenOnClick = openOnClick,
ShowUsernameTooltip = showUsernameTooltip,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
}; };
avatar.OpenOnClick.BindTo(OpenOnClick);
return avatar; return avatar;
} }
} }

View File

@ -48,11 +48,7 @@ namespace osu.Game.Users
statusIcon.FinishTransforms(); statusIcon.FinishTransforms();
} }
protected UpdateableAvatar CreateAvatar() => new UpdateableAvatar protected UpdateableAvatar CreateAvatar() => new UpdateableAvatar(User, false);
{
User = User,
OpenOnClick = { Value = false }
};
protected UpdateableFlag CreateFlag() => new UpdateableFlag(User.Country) protected UpdateableFlag CreateFlag() => new UpdateableFlag(User.Country)
{ {

View File

@ -36,8 +36,8 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Realm" Version="10.2.0" /> <PackageReference Include="Realm" Version="10.2.0" />
<PackageReference Include="ppy.osu.Framework" Version="2021.614.0" /> <PackageReference Include="ppy.osu.Framework" Version="2021.616.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.611.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.614.0" />
<PackageReference Include="Sentry" Version="3.4.0" /> <PackageReference Include="Sentry" Version="3.4.0" />
<PackageReference Include="SharpCompress" Version="0.28.2" /> <PackageReference Include="SharpCompress" Version="0.28.2" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />

View File

@ -70,8 +70,8 @@
<Reference Include="System.Net.Http" /> <Reference Include="System.Net.Http" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.614.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2021.616.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.611.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.614.0" />
</ItemGroup> </ItemGroup>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) --> <!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
<PropertyGroup> <PropertyGroup>
@ -93,7 +93,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="ppy.osu.Framework" Version="2021.614.0" /> <PackageReference Include="ppy.osu.Framework" Version="2021.616.0" />
<PackageReference Include="SharpCompress" Version="0.28.2" /> <PackageReference Include="SharpCompress" Version="0.28.2" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="SharpRaven" Version="2.4.0" /> <PackageReference Include="SharpRaven" Version="2.4.0" />