1
0
mirror of https://github.com/ppy/osu.git synced 2025-03-28 01:37:46 +08:00

Merge branch 'master' into chat-command

This commit is contained in:
rednir 2021-09-07 17:09:46 +01:00 committed by GitHub
commit f209222812
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 1107 additions and 309 deletions

163
.github/workflows/test-diffcalc.yml vendored Normal file
View File

@ -0,0 +1,163 @@
# Listens for new PR comments containing !pp check [id], and runs a diffcalc comparison against master.
# Usage:
# !pp check 0 | Runs only the osu! ruleset.
# !pp check 0 2 | Runs only the osu! and catch rulesets.
#
name: Diffcalc Consistency Checks
on:
issue_comment:
types: [ created ]
env:
DB_USER: root
DB_HOST: 127.0.0.1
CONCURRENCY: 4
ALLOW_DOWNLOAD: 1
SAVE_DOWNLOADED: 1
jobs:
diffcalc:
name: Diffcalc
runs-on: ubuntu-latest
continue-on-error: true
if: |
github.event.issue.pull_request &&
contains(github.event.comment.body, '!pp check') &&
(github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER')
strategy:
fail-fast: false
matrix:
ruleset:
- { name: osu, id: 0 }
- { name: taiko, id: 1 }
- { name: catch, id: 2 }
- { name: mania, id: 3 }
services:
mysql:
image: mysql:8.0
env:
MYSQL_ALLOW_EMPTY_PASSWORD: yes
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
steps:
- name: Verify ruleset
if: contains(github.event.comment.body, matrix.ruleset.id) == false
run: |
echo "${{ github.event.comment.body }} doesn't contain ${{ matrix.ruleset.id }}"
exit 1
- name: Verify MySQL connection from host
run: |
sudo apt-get install -y mysql-client
mysql --host ${{ env.DB_HOST }} -u${{ env.DB_USER }} -e "SHOW DATABASES"
- name: Create directory structure
run: |
mkdir -p $GITHUB_WORKSPACE/master/
mkdir -p $GITHUB_WORKSPACE/pr/
# Checkout osu
- name: Checkout osu (master)
uses: actions/checkout@v2
with:
repository: ppy/osu
path: 'master/osu'
- name: Checkout osu (pr)
uses: actions/checkout@v2
with:
path: 'pr/osu'
# Checkout osu-difficulty-calculator
- name: Checkout osu-difficulty-calculator (master)
uses: actions/checkout@v2
with:
repository: ppy/osu-difficulty-calculator
path: 'master/osu-difficulty-calculator'
- name: Checkout osu-difficulty-calculator (pr)
uses: actions/checkout@v2
with:
repository: ppy/osu-difficulty-calculator
path: 'pr/osu-difficulty-calculator'
- name: Install .NET 5.0.x
uses: actions/setup-dotnet@v1
with:
dotnet-version: "5.0.x"
# Sanity checks to make sure diffcalc is not run when incompatible.
- name: Build diffcalc (master)
run: |
cd $GITHUB_WORKSPACE/master/osu-difficulty-calculator
./UseLocalOsu.sh
dotnet build
- name: Build diffcalc (pr)
run: |
cd $GITHUB_WORKSPACE/pr/osu-difficulty-calculator
./UseLocalOsu.sh
dotnet build
# Initial data imports
- name: Download + import data
run: |
PERFORMANCE_DATA_NAME=$(curl https://data.ppy.sh/ | grep performance_${{ matrix.ruleset.name }}_top | tail -1 | awk -F "\"" '{print $2}' | sed 's/\.tar\.bz2//g')
BEATMAPS_DATA_NAME=$(curl https://data.ppy.sh/ | grep osu_files | tail -1 | awk -F "\"" '{print $2}' | sed 's/\.tar\.bz2//g')
# Set env variable for further steps.
echo "BEATMAPS_PATH=$GITHUB_WORKSPACE/$BEATMAPS_DATA_NAME" >> $GITHUB_ENV
cd $GITHUB_WORKSPACE
wget https://data.ppy.sh/$PERFORMANCE_DATA_NAME.tar.bz2
wget https://data.ppy.sh/$BEATMAPS_DATA_NAME.tar.bz2
tar -xf $PERFORMANCE_DATA_NAME.tar.bz2
tar -xf $BEATMAPS_DATA_NAME.tar.bz2
cd $GITHUB_WORKSPACE/$PERFORMANCE_DATA_NAME
mysql --host ${{ env.DB_HOST }} -u${{ env.DB_USER }} -e "CREATE DATABASE osu_master"
mysql --host ${{ env.DB_HOST }} -u${{ env.DB_USER }} -e "CREATE DATABASE osu_pr"
cat *.sql | mysql --host ${{ env.DB_HOST }} -u${{ env.DB_USER }} --database=osu_master
cat *.sql | mysql --host ${{ env.DB_HOST }} -u${{ env.DB_USER }} --database=osu_pr
# Run diffcalc
- name: Run diffcalc (master)
env:
DB_NAME: osu_master
run: |
cd $GITHUB_WORKSPACE/master/osu-difficulty-calculator/osu.Server.DifficultyCalculator
dotnet run -c:Release -- all -m ${{ matrix.ruleset.id }} -ac -c ${{ env.CONCURRENCY }}
- name: Run diffcalc (pr)
env:
DB_NAME: osu_pr
run: |
cd $GITHUB_WORKSPACE/pr/osu-difficulty-calculator/osu.Server.DifficultyCalculator
dotnet run -c:Release -- all -m ${{ matrix.ruleset.id }} -ac -c ${{ env.CONCURRENCY }}
# Print diffs
- name: Print diffs
run: |
mysql --host ${{ env.DB_HOST }} -u${{ env.DB_USER }} -e "
SELECT
m.beatmap_id,
m.mods,
m.diff_unified as 'sr_master',
p.diff_unified as 'sr_pr',
(p.diff_unified - m.diff_unified) as 'diff'
FROM osu_master.osu_beatmap_difficulty m
JOIN osu_pr.osu_beatmap_difficulty p
ON m.beatmap_id = p.beatmap_id
AND m.mode = p.mode
AND m.mods = p.mods
WHERE abs(m.diff_unified - p.diff_unified) > 0.1
ORDER BY abs(m.diff_unified - p.diff_unified)
DESC
LIMIT 10000;"
# Todo: Run ppcalc

View File

@ -51,8 +51,8 @@
<Reference Include="Java.Interop" /> <Reference Include="Java.Interop" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.827.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.907.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.830.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2021.907.0" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Transitive Dependencies"> <ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. --> <!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->

View File

@ -11,6 +11,7 @@ using osu.Framework.Platform;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Input; using osu.Game.Input;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Rulesets;
using Realms; using Realms;
namespace osu.Game.Tests.Database namespace osu.Game.Tests.Database
@ -42,7 +43,7 @@ namespace osu.Game.Tests.Database
KeyBindingContainer testContainer = new TestKeyBindingContainer(); KeyBindingContainer testContainer = new TestKeyBindingContainer();
keyBindingStore.Register(testContainer); keyBindingStore.Register(testContainer, Enumerable.Empty<RulesetInfo>());
Assert.That(queryCount(), Is.EqualTo(3)); Assert.That(queryCount(), Is.EqualTo(3));
@ -66,7 +67,7 @@ namespace osu.Game.Tests.Database
{ {
KeyBindingContainer testContainer = new TestKeyBindingContainer(); KeyBindingContainer testContainer = new TestKeyBindingContainer();
keyBindingStore.Register(testContainer); keyBindingStore.Register(testContainer, Enumerable.Empty<RulesetInfo>());
using (var primaryUsage = realmContextFactory.GetForRead()) using (var primaryUsage = realmContextFactory.GetForRead())
{ {

View File

@ -40,10 +40,10 @@ namespace osu.Game.Tests.NonVisual.Skinning
assertPlaybackPosition(0); assertPlaybackPosition(0);
AddStep("set start time to 1000", () => animationTimeReference.AnimationStartTime.Value = 1000); AddStep("set start time to 1000", () => animationTimeReference.AnimationStartTime.Value = 1000);
assertPlaybackPosition(-1000); assertPlaybackPosition(0);
AddStep("set current time to 500", () => animationTimeReference.ManualClock.CurrentTime = 500); AddStep("set current time to 500", () => animationTimeReference.ManualClock.CurrentTime = 500);
assertPlaybackPosition(-500); assertPlaybackPosition(0);
} }
private void assertPlaybackPosition(double expectedPosition) private void assertPlaybackPosition(double expectedPosition)

View File

@ -0,0 +1,170 @@
// 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.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Dialog;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components.Menus;
using osu.Game.Screens.Menu;
using osu.Game.Tests.Beatmaps.IO;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Editing
{
public class TestSceneDifficultySwitching : ScreenTestScene
{
private BeatmapSetInfo importedBeatmapSet;
private Editor editor;
// required for screen transitions to work properly
// (see comment in EditorLoader.LogoArriving).
[Cached]
private OsuLogo logo = new OsuLogo
{
Alpha = 0
};
[Resolved]
private OsuGameBase game { get; set; }
[Resolved]
private BeatmapManager beatmaps { get; set; }
[BackgroundDependencyLoader]
private void load() => Add(logo);
[SetUpSteps]
public void SetUp()
{
AddStep("import test beatmap", () => importedBeatmapSet = ImportBeatmapTest.LoadOszIntoOsu(game, virtualTrack: true).Result);
AddStep("set current beatmap", () => Beatmap.Value = beatmaps.GetWorkingBeatmap(importedBeatmapSet.Beatmaps.First()));
AddStep("push loader", () => Stack.Push(new EditorLoader()));
AddUntilStep("wait for editor push", () => Stack.CurrentScreen is Editor);
AddStep("store editor", () => editor = (Editor)Stack.CurrentScreen);
AddUntilStep("wait for editor to load", () => editor.IsLoaded);
}
[Test]
public void TestBasicSwitch()
{
BeatmapInfo targetDifficulty = null;
AddStep("set target difficulty", () => targetDifficulty = importedBeatmapSet.Beatmaps.Last(beatmap => !beatmap.Equals(Beatmap.Value.BeatmapInfo)));
switchToDifficulty(() => targetDifficulty);
confirmEditingBeatmap(() => targetDifficulty);
AddStep("exit editor", () => Stack.Exit());
// ensure editor loader didn't resume.
AddAssert("stack empty", () => Stack.CurrentScreen == null);
}
[Test]
public void TestPreventSwitchDueToUnsavedChanges()
{
BeatmapInfo targetDifficulty = null;
PromptForSaveDialog saveDialog = null;
AddStep("remove first hitobject", () =>
{
var editorBeatmap = editor.ChildrenOfType<EditorBeatmap>().Single();
editorBeatmap.RemoveAt(0);
});
AddStep("set target difficulty", () => targetDifficulty = importedBeatmapSet.Beatmaps.Last(beatmap => !beatmap.Equals(Beatmap.Value.BeatmapInfo)));
switchToDifficulty(() => targetDifficulty);
AddUntilStep("prompt for save dialog shown", () =>
{
saveDialog = this.ChildrenOfType<PromptForSaveDialog>().Single();
return saveDialog != null;
});
AddStep("continue editing", () =>
{
var continueButton = saveDialog.ChildrenOfType<PopupDialogCancelButton>().Last();
continueButton.TriggerClick();
});
confirmEditingBeatmap(() => importedBeatmapSet.Beatmaps.First());
AddRepeatStep("exit editor forcefully", () => Stack.Exit(), 2);
// ensure editor loader didn't resume.
AddAssert("stack empty", () => Stack.CurrentScreen == null);
}
[Test]
public void TestAllowSwitchAfterDiscardingUnsavedChanges()
{
BeatmapInfo targetDifficulty = null;
PromptForSaveDialog saveDialog = null;
AddStep("remove first hitobject", () =>
{
var editorBeatmap = editor.ChildrenOfType<EditorBeatmap>().Single();
editorBeatmap.RemoveAt(0);
});
AddStep("set target difficulty", () => targetDifficulty = importedBeatmapSet.Beatmaps.Last(beatmap => !beatmap.Equals(Beatmap.Value.BeatmapInfo)));
switchToDifficulty(() => targetDifficulty);
AddUntilStep("prompt for save dialog shown", () =>
{
saveDialog = this.ChildrenOfType<PromptForSaveDialog>().Single();
return saveDialog != null;
});
AddStep("discard changes", () =>
{
var continueButton = saveDialog.ChildrenOfType<PopupDialogOkButton>().Single();
continueButton.TriggerClick();
});
confirmEditingBeatmap(() => targetDifficulty);
AddStep("exit editor forcefully", () => Stack.Exit());
// ensure editor loader didn't resume.
AddAssert("stack empty", () => Stack.CurrentScreen == null);
}
private void switchToDifficulty(Func<BeatmapInfo> difficulty)
{
AddUntilStep("wait for menubar to load", () => editor.ChildrenOfType<EditorMenuBar>().Any());
AddStep("open file menu", () =>
{
var menuBar = editor.ChildrenOfType<EditorMenuBar>().Single();
var fileMenu = menuBar.ChildrenOfType<DrawableOsuMenuItem>().First();
InputManager.MoveMouseTo(fileMenu);
InputManager.Click(MouseButton.Left);
});
AddStep("open difficulty menu", () =>
{
var difficultySelector =
editor.ChildrenOfType<DrawableOsuMenuItem>().Single(item => item.Item.Text.Value.ToString().Contains("Change difficulty"));
InputManager.MoveMouseTo(difficultySelector);
});
AddWaitStep("wait for open", 3);
AddStep("switch to target difficulty", () =>
{
var difficultyMenuItem =
editor.ChildrenOfType<DrawableOsuMenuItem>()
.Last(item => item.Item is DifficultyMenuItem difficultyItem && difficultyItem.Beatmap.Equals(difficulty.Invoke()));
InputManager.MoveMouseTo(difficultyMenuItem);
InputManager.Click(MouseButton.Left);
});
}
private void confirmEditingBeatmap(Func<BeatmapInfo> targetDifficulty)
{
AddUntilStep("current beatmap is correct", () => Beatmap.Value.BeatmapInfo.Equals(targetDifficulty.Invoke()));
AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
}
}
}

View File

@ -160,11 +160,14 @@ namespace osu.Game.Tests.Visual.Playlists
Ruleset = { Value = new OsuRuleset().RulesetInfo } Ruleset = { Value = new OsuRuleset().RulesetInfo }
})); }));
}); });
AddUntilStep("wait for load", () => resultsScreen.ChildrenOfType<ScorePanelList>().FirstOrDefault()?.AllPanelsVisible == true);
} }
private void waitForDisplay() private void waitForDisplay()
{ {
AddUntilStep("wait for request to complete", () => requestComplete); AddUntilStep("wait for request to complete", () => requestComplete);
AddUntilStep("wait for panels to be visible", () => resultsScreen.ChildrenOfType<ScorePanelList>().FirstOrDefault()?.AllPanelsVisible == true);
AddWaitStep("wait for display", 5); AddWaitStep("wait for display", 5);
} }

View File

@ -99,7 +99,7 @@ namespace osu.Game.Tests.Visual.Ranking
TestResultsScreen screen = null; TestResultsScreen screen = null;
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen())); AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
AddUntilStep("wait for loaded", () => screen.IsLoaded); AddUntilStep("wait for load", () => this.ChildrenOfType<ScorePanelList>().Single().AllPanelsVisible);
AddStep("click expanded panel", () => AddStep("click expanded panel", () =>
{ {
@ -138,7 +138,7 @@ namespace osu.Game.Tests.Visual.Ranking
TestResultsScreen screen = null; TestResultsScreen screen = null;
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen())); AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
AddUntilStep("wait for loaded", () => screen.IsLoaded); AddUntilStep("wait for load", () => this.ChildrenOfType<ScorePanelList>().Single().AllPanelsVisible);
AddStep("click expanded panel", () => AddStep("click expanded panel", () =>
{ {
@ -177,7 +177,7 @@ namespace osu.Game.Tests.Visual.Ranking
TestResultsScreen screen = null; TestResultsScreen screen = null;
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen())); AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
AddUntilStep("wait for loaded", () => screen.IsLoaded); AddUntilStep("wait for load", () => this.ChildrenOfType<ScorePanelList>().Single().AllPanelsVisible);
ScorePanel expandedPanel = null; ScorePanel expandedPanel = null;
ScorePanel contractedPanel = null; ScorePanel contractedPanel = null;
@ -223,6 +223,7 @@ namespace osu.Game.Tests.Visual.Ranking
TestResultsScreen screen = null; TestResultsScreen screen = null;
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen())); AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
AddUntilStep("wait for load", () => this.ChildrenOfType<ScorePanelList>().Single().AllPanelsVisible);
AddAssert("download button is disabled", () => !screen.ChildrenOfType<DownloadButton>().Last().Enabled.Value); AddAssert("download button is disabled", () => !screen.ChildrenOfType<DownloadButton>().Last().Enabled.Value);

View File

@ -159,6 +159,9 @@ namespace osu.Game.Tests.Visual.Ranking
var firstScore = new TestScoreInfo(new OsuRuleset().RulesetInfo); var firstScore = new TestScoreInfo(new OsuRuleset().RulesetInfo);
var secondScore = new TestScoreInfo(new OsuRuleset().RulesetInfo); var secondScore = new TestScoreInfo(new OsuRuleset().RulesetInfo);
firstScore.User.Username = "A";
secondScore.User.Username = "B";
createListStep(() => new ScorePanelList()); createListStep(() => new ScorePanelList());
AddStep("add scores and select first", () => AddStep("add scores and select first", () =>
@ -168,6 +171,8 @@ namespace osu.Game.Tests.Visual.Ranking
list.SelectedScore.Value = firstScore; list.SelectedScore.Value = firstScore;
}); });
AddUntilStep("wait for load", () => list.AllPanelsVisible);
assertScoreState(firstScore, true); assertScoreState(firstScore, true);
assertScoreState(secondScore, false); assertScoreState(secondScore, false);
@ -182,6 +187,22 @@ namespace osu.Game.Tests.Visual.Ranking
assertExpandedPanelCentred(); assertExpandedPanelCentred();
} }
[Test]
public void TestAddScoreImmediately()
{
var score = new TestScoreInfo(new OsuRuleset().RulesetInfo);
createListStep(() =>
{
var newList = new ScorePanelList { SelectedScore = { Value = score } };
newList.AddScore(score);
return newList;
});
assertScoreState(score, true);
assertExpandedPanelCentred();
}
private void createListStep(Func<ScorePanelList> creationFunc) private void createListStep(Func<ScorePanelList> creationFunc)
{ {
AddStep("create list", () => Child = list = creationFunc().With(d => AddStep("create list", () => Child = list = creationFunc().With(d =>

View File

@ -1,14 +1,15 @@
// 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.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Input.Handlers.Tablet; using osu.Framework.Input.Handlers.Tablet;
using osu.Framework.Platform; using osu.Framework.Testing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Settings;
using osu.Game.Overlays.Settings.Sections.Input; using osu.Game.Overlays.Settings.Sections.Input;
using osuTK; using osuTK;
@ -17,22 +18,34 @@ namespace osu.Game.Tests.Visual.Settings
[TestFixture] [TestFixture]
public class TestSceneTabletSettings : OsuTestScene public class TestSceneTabletSettings : OsuTestScene
{ {
[BackgroundDependencyLoader] private TestTabletHandler tabletHandler;
private void load(GameHost host) private TabletSettings settings;
{
var tabletHandler = new TestTabletHandler();
AddRange(new Drawable[] [SetUpSteps]
public void SetUpSteps()
{
AddStep("create settings", () =>
{ {
new TabletSettings(tabletHandler) tabletHandler = new TestTabletHandler();
Children = new Drawable[]
{ {
RelativeSizeAxes = Axes.None, settings = new TabletSettings(tabletHandler)
Width = SettingsPanel.PANEL_WIDTH, {
Anchor = Anchor.TopCentre, RelativeSizeAxes = Axes.None,
Origin = Anchor.TopCentre, Width = SettingsPanel.PANEL_WIDTH,
} Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
}
};
}); });
AddStep("set square size", () => tabletHandler.SetTabletSize(new Vector2(100, 100)));
}
[Test]
public void TestVariousTabletSizes()
{
AddStep("Test with wide tablet", () => tabletHandler.SetTabletSize(new Vector2(160, 100))); AddStep("Test with wide tablet", () => tabletHandler.SetTabletSize(new Vector2(160, 100)));
AddStep("Test with square tablet", () => tabletHandler.SetTabletSize(new Vector2(300, 300))); AddStep("Test with square tablet", () => tabletHandler.SetTabletSize(new Vector2(300, 300)));
AddStep("Test with tall tablet", () => tabletHandler.SetTabletSize(new Vector2(100, 300))); AddStep("Test with tall tablet", () => tabletHandler.SetTabletSize(new Vector2(100, 300)));
@ -40,6 +53,71 @@ namespace osu.Game.Tests.Visual.Settings
AddStep("Test no tablet present", () => tabletHandler.SetTabletSize(Vector2.Zero)); AddStep("Test no tablet present", () => tabletHandler.SetTabletSize(Vector2.Zero));
} }
[Test]
public void TestWideAspectRatioValidity()
{
AddStep("Test with wide tablet", () => tabletHandler.SetTabletSize(new Vector2(160, 100)));
AddStep("Reset to full area", () => settings.ChildrenOfType<DangerousSettingsButton>().First().TriggerClick());
ensureValid();
AddStep("rotate 10", () => tabletHandler.Rotation.Value = 10);
ensureInvalid();
AddStep("scale down", () => tabletHandler.AreaSize.Value *= 0.9f);
ensureInvalid();
AddStep("scale down", () => tabletHandler.AreaSize.Value *= 0.9f);
ensureInvalid();
AddStep("scale down", () => tabletHandler.AreaSize.Value *= 0.9f);
ensureValid();
}
[Test]
public void TestRotationValidity()
{
AddAssert("area valid", () => settings.AreaSelection.IsWithinBounds);
AddStep("rotate 90", () => tabletHandler.Rotation.Value = 90);
ensureValid();
AddStep("rotate 180", () => tabletHandler.Rotation.Value = 180);
ensureValid();
AddStep("rotate 270", () => tabletHandler.Rotation.Value = 270);
ensureValid();
AddStep("rotate 360", () => tabletHandler.Rotation.Value = 360);
ensureValid();
AddStep("rotate 0", () => tabletHandler.Rotation.Value = 0);
ensureValid();
AddStep("rotate 45", () => tabletHandler.Rotation.Value = 45);
ensureInvalid();
AddStep("rotate 0", () => tabletHandler.Rotation.Value = 0);
ensureValid();
}
[Test]
public void TestOffsetValidity()
{
ensureValid();
AddStep("move right", () => tabletHandler.AreaOffset.Value = Vector2.Zero);
ensureInvalid();
AddStep("move back", () => tabletHandler.AreaOffset.Value = tabletHandler.AreaSize.Value / 2);
ensureValid();
}
private void ensureValid() => AddAssert("area valid", () => settings.AreaSelection.IsWithinBounds);
private void ensureInvalid() => AddAssert("area invalid", () => !settings.AreaSelection.IsWithinBounds);
public class TestTabletHandler : ITabletHandler public class TestTabletHandler : ITabletHandler
{ {
public Bindable<Vector2> AreaOffset { get; } = new Bindable<Vector2>(); public Bindable<Vector2> AreaOffset { get; } = new Bindable<Vector2>();

View File

@ -41,7 +41,7 @@ namespace osu.Game.Tests.Visual.SongSelect
dependencies.Cache(rulesetStore = new RulesetStore(ContextFactory)); dependencies.Cache(rulesetStore = new RulesetStore(ContextFactory));
dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesetStore, null, dependencies.Get<AudioManager>(), Resources, dependencies.Get<GameHost>(), Beatmap.Default)); dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesetStore, null, dependencies.Get<AudioManager>(), Resources, dependencies.Get<GameHost>(), Beatmap.Default));
dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, null, ContextFactory)); dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, null, ContextFactory, Scheduler));
return dependencies; return dependencies;
} }

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.Linq; using System.Linq;
using Humanizer;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -73,7 +74,7 @@ namespace osu.Game.Tests.Visual.UserInterface
}; };
control.Query.BindValueChanged(q => query.Text = $"Query: {q.NewValue}", true); control.Query.BindValueChanged(q => query.Text = $"Query: {q.NewValue}", true);
control.General.BindCollectionChanged((u, v) => general.Text = $"General: {(control.General.Any() ? string.Join('.', control.General.Select(i => i.ToString().ToLowerInvariant())) : "")}", true); control.General.BindCollectionChanged((u, v) => general.Text = $"General: {(control.General.Any() ? string.Join('.', control.General.Select(i => i.ToString().Underscore())) : "")}", true);
control.Ruleset.BindValueChanged(r => ruleset.Text = $"Ruleset: {r.NewValue}", true); control.Ruleset.BindValueChanged(r => ruleset.Text = $"Ruleset: {r.NewValue}", true);
control.Category.BindValueChanged(c => category.Text = $"Category: {c.NewValue}", true); control.Category.BindValueChanged(c => category.Text = $"Category: {c.NewValue}", true);
control.Genre.BindValueChanged(g => genre.Text = $"Genre: {g.NewValue}", true); control.Genre.BindValueChanged(g => genre.Text = $"Genre: {g.NewValue}", true);

View File

@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.UserInterface
private BeatmapManager beatmapManager; private BeatmapManager beatmapManager;
private ScoreManager scoreManager; private ScoreManager scoreManager;
private readonly List<ScoreInfo> scores = new List<ScoreInfo>(); private readonly List<ScoreInfo> importedScores = new List<ScoreInfo>();
private BeatmapInfo beatmap; private BeatmapInfo beatmap;
[Cached] [Cached]
@ -82,7 +82,7 @@ namespace osu.Game.Tests.Visual.UserInterface
dependencies.Cache(rulesetStore = new RulesetStore(ContextFactory)); dependencies.Cache(rulesetStore = new RulesetStore(ContextFactory));
dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesetStore, null, dependencies.Get<AudioManager>(), Resources, dependencies.Get<GameHost>(), Beatmap.Default)); dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesetStore, null, dependencies.Get<AudioManager>(), Resources, dependencies.Get<GameHost>(), Beatmap.Default));
dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, null, ContextFactory)); dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, null, ContextFactory, Scheduler));
beatmap = beatmapManager.Import(new ImportTask(TestResources.GetQuickTestBeatmapForImport())).Result.Beatmaps[0]; beatmap = beatmapManager.Import(new ImportTask(TestResources.GetQuickTestBeatmapForImport())).Result.Beatmaps[0];
@ -100,11 +100,9 @@ namespace osu.Game.Tests.Visual.UserInterface
User = new User { Username = "TestUser" }, User = new User { Username = "TestUser" },
}; };
scores.Add(scoreManager.Import(score).Result); importedScores.Add(scoreManager.Import(score).Result);
} }
scores.Sort(Comparer<ScoreInfo>.Create((s1, s2) => s2.TotalScore.CompareTo(s1.TotalScore)));
return dependencies; return dependencies;
} }
@ -134,9 +132,14 @@ namespace osu.Game.Tests.Visual.UserInterface
[Test] [Test]
public void TestDeleteViaRightClick() public void TestDeleteViaRightClick()
{ {
ScoreInfo scoreBeingDeleted = null;
AddStep("open menu for top score", () => AddStep("open menu for top score", () =>
{ {
InputManager.MoveMouseTo(leaderboard.ChildrenOfType<LeaderboardScore>().First()); var leaderboardScore = leaderboard.ChildrenOfType<LeaderboardScore>().First();
scoreBeingDeleted = leaderboardScore.Score;
InputManager.MoveMouseTo(leaderboardScore);
InputManager.Click(MouseButton.Right); InputManager.Click(MouseButton.Right);
}); });
@ -158,14 +161,14 @@ namespace osu.Game.Tests.Visual.UserInterface
InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Left);
}); });
AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineScoreID != scores[0].OnlineScoreID)); AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineScoreID != scoreBeingDeleted.OnlineScoreID));
} }
[Test] [Test]
public void TestDeleteViaDatabase() public void TestDeleteViaDatabase()
{ {
AddStep("delete top score", () => scoreManager.Delete(scores[0])); AddStep("delete top score", () => scoreManager.Delete(importedScores[0]));
AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineScoreID != scores[0].OnlineScoreID)); AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineScoreID != importedScores[0].OnlineScoreID));
} }
} }
} }

View File

@ -46,52 +46,53 @@ namespace osu.Game.Input
} }
/// <summary> /// <summary>
/// Register a new type of <see cref="KeyBindingContainer{T}"/>, adding default bindings from <see cref="KeyBindingContainer.DefaultKeyBindings"/>. /// Register all defaults for this store.
/// </summary> /// </summary>
/// <param name="container">The container to populate defaults from.</param> /// <param name="container">The container to populate defaults from.</param>
public void Register(KeyBindingContainer container) => insertDefaults(container.DefaultKeyBindings); /// <param name="rulesets">The rulesets to populate defaults from.</param>
public void Register(KeyBindingContainer container, IEnumerable<RulesetInfo> rulesets)
/// <summary>
/// Register a ruleset, adding default bindings for each of its variants.
/// </summary>
/// <param name="ruleset">The ruleset to populate defaults from.</param>
public void Register(RulesetInfo ruleset)
{
var instance = ruleset.CreateInstance();
foreach (var variant in instance.AvailableVariants)
insertDefaults(instance.GetDefaultKeyBindings(variant), ruleset.ID, variant);
}
private void insertDefaults(IEnumerable<IKeyBinding> defaults, int? rulesetId = null, int? variant = null)
{ {
using (var usage = realmFactory.GetForWrite()) using (var usage = realmFactory.GetForWrite())
{ {
// compare counts in database vs defaults // intentionally flattened to a list rather than querying against the IQueryable, as nullable fields being queried against aren't indexed.
foreach (var defaultsForAction in defaults.GroupBy(k => k.Action)) // this is much faster as a result.
var existingBindings = usage.Realm.All<RealmKeyBinding>().ToList();
insertDefaults(usage, existingBindings, container.DefaultKeyBindings);
foreach (var ruleset in rulesets)
{ {
int existingCount = usage.Realm.All<RealmKeyBinding>().Count(k => k.RulesetID == rulesetId && k.Variant == variant && k.ActionInt == (int)defaultsForAction.Key); var instance = ruleset.CreateInstance();
foreach (var variant in instance.AvailableVariants)
if (defaultsForAction.Count() <= existingCount) insertDefaults(usage, existingBindings, instance.GetDefaultKeyBindings(variant), ruleset.ID, variant);
continue;
foreach (var k in defaultsForAction.Skip(existingCount))
{
// insert any defaults which are missing.
usage.Realm.Add(new RealmKeyBinding
{
KeyCombinationString = k.KeyCombination.ToString(),
ActionInt = (int)k.Action,
RulesetID = rulesetId,
Variant = variant
});
}
} }
usage.Commit(); usage.Commit();
} }
} }
private void insertDefaults(RealmContextFactory.RealmUsage usage, List<RealmKeyBinding> existingBindings, IEnumerable<IKeyBinding> defaults, int? rulesetId = null, int? variant = null)
{
// compare counts in database vs defaults for each action type.
foreach (var defaultsForAction in defaults.GroupBy(k => k.Action))
{
// avoid performing redundant queries when the database is empty and needs to be re-filled.
int existingCount = existingBindings.Count(k => k.RulesetID == rulesetId && k.Variant == variant && k.ActionInt == (int)defaultsForAction.Key);
if (defaultsForAction.Count() <= existingCount)
continue;
// insert any defaults which are missing.
usage.Realm.Add(defaultsForAction.Skip(existingCount).Select(k => new RealmKeyBinding
{
KeyCombinationString = k.KeyCombination.ToString(),
ActionInt = (int)k.Action,
RulesetID = rulesetId,
Variant = variant
}));
}
}
/// <summary> /// <summary>
/// Keys which should not be allowed for gameplay input purposes. /// Keys which should not be allowed for gameplay input purposes.
/// </summary> /// </summary>

View File

@ -8,8 +8,9 @@ namespace osu.Game.Online.API.Requests
{ {
public class GetUserRequest : APIRequest<User> public class GetUserRequest : APIRequest<User>
{ {
private readonly string userIdentifier; private readonly string lookup;
public readonly RulesetInfo Ruleset; public readonly RulesetInfo Ruleset;
private readonly LookupType lookupType;
/// <summary> /// <summary>
/// Gets the currently logged-in user. /// Gets the currently logged-in user.
@ -25,7 +26,8 @@ namespace osu.Game.Online.API.Requests
/// <param name="ruleset">The ruleset to get the user's info for.</param> /// <param name="ruleset">The ruleset to get the user's info for.</param>
public GetUserRequest(long? userId = null, RulesetInfo ruleset = null) public GetUserRequest(long? userId = null, RulesetInfo ruleset = null)
{ {
this.userIdentifier = userId.ToString(); lookup = userId.ToString();
lookupType = LookupType.Id;
Ruleset = ruleset; Ruleset = ruleset;
} }
@ -36,10 +38,17 @@ namespace osu.Game.Online.API.Requests
/// <param name="ruleset">The ruleset to get the user's info for.</param> /// <param name="ruleset">The ruleset to get the user's info for.</param>
public GetUserRequest(string username = null, RulesetInfo ruleset = null) public GetUserRequest(string username = null, RulesetInfo ruleset = null)
{ {
this.userIdentifier = username; lookup = username;
lookupType = LookupType.Username;
Ruleset = ruleset; Ruleset = ruleset;
} }
protected override string Target => userIdentifier != null ? $@"users/{userIdentifier}/{Ruleset?.ShortName}" : $@"me/{Ruleset?.ShortName}"; protected override string Target => lookup != null ? $@"users/{lookup}/{Ruleset?.ShortName}?k={lookupType.ToString().ToLower()}" : $@"me/{Ruleset?.ShortName}";
private enum LookupType
{
Id,
Username
}
} }
} }

View File

@ -3,6 +3,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Humanizer;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.IO.Network; using osu.Framework.IO.Network;
using osu.Game.Extensions; using osu.Game.Extensions;
@ -83,7 +84,7 @@ namespace osu.Game.Online.API.Requests
req.AddParameter("q", query); req.AddParameter("q", query);
if (General != null && General.Any()) if (General != null && General.Any())
req.AddParameter("c", string.Join('.', General.Select(e => e.ToString().ToLowerInvariant()))); req.AddParameter("c", string.Join('.', General.Select(e => e.ToString().Underscore())));
if (ruleset.ID.HasValue) if (ruleset.ID.HasValue)
req.AddParameter("m", ruleset.ID.Value.ToString()); req.AddParameter("m", ruleset.ID.Value.ToString());

View File

@ -34,6 +34,8 @@ namespace osu.Game.Online.Leaderboards
{ {
public const float HEIGHT = 60; public const float HEIGHT = 60;
public readonly ScoreInfo Score;
private const float corner_radius = 5; private const float corner_radius = 5;
private const float edge_margin = 5; private const float edge_margin = 5;
private const float background_alpha = 0.25f; private const float background_alpha = 0.25f;
@ -41,7 +43,6 @@ namespace osu.Game.Online.Leaderboards
protected Container RankContainer { get; private set; } protected Container RankContainer { get; private set; }
private readonly ScoreInfo score;
private readonly int? rank; private readonly int? rank;
private readonly bool allowHighlight; private readonly bool allowHighlight;
@ -67,7 +68,8 @@ namespace osu.Game.Online.Leaderboards
public LeaderboardScore(ScoreInfo score, int? rank, bool allowHighlight = true) public LeaderboardScore(ScoreInfo score, int? rank, bool allowHighlight = true)
{ {
this.score = score; Score = score;
this.rank = rank; this.rank = rank;
this.allowHighlight = allowHighlight; this.allowHighlight = allowHighlight;
@ -78,9 +80,9 @@ namespace osu.Game.Online.Leaderboards
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(IAPIProvider api, OsuColour colour, ScoreManager scoreManager) private void load(IAPIProvider api, OsuColour colour, ScoreManager scoreManager)
{ {
var user = score.User; var user = Score.User;
statisticsLabels = GetStatistics(score).Select(s => new ScoreComponentLabel(s)).ToList(); statisticsLabels = GetStatistics(Score).Select(s => new ScoreComponentLabel(s)).ToList();
ClickableAvatar innerAvatar; ClickableAvatar innerAvatar;
@ -198,7 +200,7 @@ namespace osu.Game.Online.Leaderboards
{ {
TextColour = Color4.White, TextColour = Color4.White,
GlowColour = Color4Extensions.FromHex(@"83ccfa"), GlowColour = Color4Extensions.FromHex(@"83ccfa"),
Current = scoreManager.GetBindableTotalScoreString(score), Current = scoreManager.GetBindableTotalScoreString(Score),
Font = OsuFont.Numeric.With(size: 23), Font = OsuFont.Numeric.With(size: 23),
}, },
RankContainer = new Container RankContainer = new Container
@ -206,7 +208,7 @@ namespace osu.Game.Online.Leaderboards
Size = new Vector2(40f, 20f), Size = new Vector2(40f, 20f),
Children = new[] Children = new[]
{ {
scoreRank = new UpdateableRank(score.Rank) scoreRank = new UpdateableRank(Score.Rank)
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -223,7 +225,7 @@ namespace osu.Game.Online.Leaderboards
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal, Direction = FillDirection.Horizontal,
Spacing = new Vector2(1), Spacing = new Vector2(1),
ChildrenEnumerable = score.Mods.Select(mod => new ModIcon(mod) { Scale = new Vector2(0.375f) }) ChildrenEnumerable = Score.Mods.Select(mod => new ModIcon(mod) { Scale = new Vector2(0.375f) })
}, },
}, },
}, },
@ -389,14 +391,14 @@ namespace osu.Game.Online.Leaderboards
{ {
List<MenuItem> items = new List<MenuItem>(); List<MenuItem> items = new List<MenuItem>();
if (score.Mods.Length > 0 && modsContainer.Any(s => s.IsHovered) && songSelect != null) if (Score.Mods.Length > 0 && modsContainer.Any(s => s.IsHovered) && songSelect != null)
items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = score.Mods)); items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = Score.Mods));
if (score.Files?.Count > 0) if (Score.Files?.Count > 0)
items.Add(new OsuMenuItem("Export", MenuItemType.Standard, () => scoreManager.Export(score))); items.Add(new OsuMenuItem("Export", MenuItemType.Standard, () => scoreManager.Export(Score)));
if (score.ID != 0) if (Score.ID != 0)
items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(score)))); items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(Score))));
return items.ToArray(); return items.ToArray();
} }

View File

@ -205,31 +205,7 @@ namespace osu.Game
dependencies.CacheAs(this); dependencies.CacheAs(this);
dependencies.CacheAs(LocalConfig); dependencies.CacheAs(LocalConfig);
AddFont(Resources, @"Fonts/osuFont"); InitialiseFonts();
AddFont(Resources, @"Fonts/Torus/Torus-Regular");
AddFont(Resources, @"Fonts/Torus/Torus-Light");
AddFont(Resources, @"Fonts/Torus/Torus-SemiBold");
AddFont(Resources, @"Fonts/Torus/Torus-Bold");
AddFont(Resources, @"Fonts/Inter/Inter-Regular");
AddFont(Resources, @"Fonts/Inter/Inter-RegularItalic");
AddFont(Resources, @"Fonts/Inter/Inter-Light");
AddFont(Resources, @"Fonts/Inter/Inter-LightItalic");
AddFont(Resources, @"Fonts/Inter/Inter-SemiBold");
AddFont(Resources, @"Fonts/Inter/Inter-SemiBoldItalic");
AddFont(Resources, @"Fonts/Inter/Inter-Bold");
AddFont(Resources, @"Fonts/Inter/Inter-BoldItalic");
AddFont(Resources, @"Fonts/Noto/Noto-Basic");
AddFont(Resources, @"Fonts/Noto/Noto-Hangul");
AddFont(Resources, @"Fonts/Noto/Noto-CJK-Basic");
AddFont(Resources, @"Fonts/Noto/Noto-CJK-Compatibility");
AddFont(Resources, @"Fonts/Noto/Noto-Thai");
AddFont(Resources, @"Fonts/Venera/Venera-Light");
AddFont(Resources, @"Fonts/Venera/Venera-Bold");
AddFont(Resources, @"Fonts/Venera/Venera-Black");
Audio.Samples.PlaybackConcurrency = SAMPLE_CONCURRENCY; Audio.Samples.PlaybackConcurrency = SAMPLE_CONCURRENCY;
@ -267,7 +243,7 @@ namespace osu.Game
dependencies.Cache(fileStore = new FileStore(contextFactory, Storage)); dependencies.Cache(fileStore = new FileStore(contextFactory, Storage));
// ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup() // ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup()
dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, API, contextFactory, Host, () => difficultyCache, LocalConfig)); dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, API, contextFactory, Scheduler, Host, () => difficultyCache, LocalConfig));
dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, contextFactory, RulesetStore, API, Audio, Resources, Host, defaultBeatmap, true)); dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, contextFactory, RulesetStore, API, Audio, Resources, Host, defaultBeatmap, true));
// this should likely be moved to ArchiveModelManager when another case appears where it is necessary // this should likely be moved to ArchiveModelManager when another case appears where it is necessary
@ -351,10 +327,7 @@ namespace osu.Game
base.Content.Add(CreateScalingContainer().WithChildren(mainContent)); base.Content.Add(CreateScalingContainer().WithChildren(mainContent));
KeyBindingStore = new RealmKeyBindingStore(realmFactory); KeyBindingStore = new RealmKeyBindingStore(realmFactory);
KeyBindingStore.Register(globalBindings); KeyBindingStore.Register(globalBindings, RulesetStore.AvailableRulesets);
foreach (var r in RulesetStore.AvailableRulesets)
KeyBindingStore.Register(r);
dependencies.Cache(globalBindings); dependencies.Cache(globalBindings);
@ -368,6 +341,35 @@ namespace osu.Game
Ruleset.BindValueChanged(onRulesetChanged); Ruleset.BindValueChanged(onRulesetChanged);
} }
protected virtual void InitialiseFonts()
{
AddFont(Resources, @"Fonts/osuFont");
AddFont(Resources, @"Fonts/Torus/Torus-Regular");
AddFont(Resources, @"Fonts/Torus/Torus-Light");
AddFont(Resources, @"Fonts/Torus/Torus-SemiBold");
AddFont(Resources, @"Fonts/Torus/Torus-Bold");
AddFont(Resources, @"Fonts/Inter/Inter-Regular");
AddFont(Resources, @"Fonts/Inter/Inter-RegularItalic");
AddFont(Resources, @"Fonts/Inter/Inter-Light");
AddFont(Resources, @"Fonts/Inter/Inter-LightItalic");
AddFont(Resources, @"Fonts/Inter/Inter-SemiBold");
AddFont(Resources, @"Fonts/Inter/Inter-SemiBoldItalic");
AddFont(Resources, @"Fonts/Inter/Inter-Bold");
AddFont(Resources, @"Fonts/Inter/Inter-BoldItalic");
AddFont(Resources, @"Fonts/Noto/Noto-Basic");
AddFont(Resources, @"Fonts/Noto/Noto-Hangul");
AddFont(Resources, @"Fonts/Noto/Noto-CJK-Basic");
AddFont(Resources, @"Fonts/Noto/Noto-CJK-Compatibility");
AddFont(Resources, @"Fonts/Noto/Noto-Thai");
AddFont(Resources, @"Fonts/Venera/Venera-Light");
AddFont(Resources, @"Fonts/Venera/Venera-Bold");
AddFont(Resources, @"Fonts/Venera/Venera-Black");
}
private IDisposable blocking; private IDisposable blocking;
private void updateThreadStateChanged(ValueChangedEvent<GameThreadState> state) private void updateThreadStateChanged(ValueChangedEvent<GameThreadState> state)

View File

@ -127,7 +127,7 @@ 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>(BeatmapsStrings.ListingSearchFiltersGeneral), generalFilter = new BeatmapSearchGeneralFilterRow(),
modeFilter = new BeatmapSearchRulesetFilterRow(), modeFilter = new BeatmapSearchRulesetFilterRow(),
categoryFilter = new BeatmapSearchFilterRow<SearchCategory>(BeatmapsStrings.ListingSearchFiltersStatus), categoryFilter = new BeatmapSearchFilterRow<SearchCategory>(BeatmapsStrings.ListingSearchFiltersStatus),
genreFilter = new BeatmapSearchFilterRow<SearchGenre>(BeatmapsStrings.ListingSearchFiltersGenre), genreFilter = new BeatmapSearchFilterRow<SearchGenre>(BeatmapsStrings.ListingSearchFiltersGenre),

View File

@ -0,0 +1,39 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Resources.Localisation.Web;
using osuTK.Graphics;
namespace osu.Game.Overlays.BeatmapListing
{
public class BeatmapSearchGeneralFilterRow : BeatmapSearchMultipleSelectionFilterRow<SearchGeneral>
{
public BeatmapSearchGeneralFilterRow()
: base(BeatmapsStrings.ListingSearchFiltersGeneral)
{
}
protected override MultipleSelectionFilter CreateMultipleSelectionFilter() => new GeneralFilter();
private class GeneralFilter : MultipleSelectionFilter
{
protected override MultipleSelectionFilterTabItem CreateTabItem(SearchGeneral value)
{
if (value == SearchGeneral.FeaturedArtists)
return new FeaturedArtistsTabItem();
return new MultipleSelectionFilterTabItem(value);
}
}
private class FeaturedArtistsTabItem : MultipleSelectionFilterTabItem
{
public FeaturedArtistsTabItem()
: base(SearchGeneral.FeaturedArtists)
{
}
protected override Color4 GetStateColour() => OverlayColourProvider.Orange.Colour1;
}
}
}

View File

@ -71,10 +71,10 @@ namespace osu.Game.Overlays.BeatmapListing
private void updateState() private void updateState()
{ {
text.FadeColour(IsHovered ? colourProvider.Light1 : getStateColour(), 200, Easing.OutQuint); text.FadeColour(IsHovered ? colourProvider.Light1 : GetStateColour(), 200, Easing.OutQuint);
text.Font = text.Font.With(weight: Active.Value ? FontWeight.SemiBold : FontWeight.Regular); text.Font = text.Font.With(weight: Active.Value ? FontWeight.SemiBold : FontWeight.Regular);
} }
private Color4 getStateColour() => Active.Value ? colourProvider.Content1 : colourProvider.Light2; protected virtual Color4 GetStateColour() => Active.Value ? colourProvider.Content1 : colourProvider.Light2;
} }
} }

View File

@ -19,6 +19,10 @@ namespace osu.Game.Overlays.BeatmapListing
[LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.GeneralFollows))] [LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.GeneralFollows))]
[Description("Subscribed mappers")] [Description("Subscribed mappers")]
Follows Follows,
[LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.GeneralFeaturedArtists))]
[Description("Featured artists")]
FeaturedArtists
} }
} }

View File

@ -7,6 +7,8 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osuTK; using osuTK;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Online.API; using osu.Game.Online.API;
@ -14,6 +16,7 @@ using osu.Game.Online.API.Requests;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Scoring;
using osu.Game.Screens.Select.Leaderboards; using osu.Game.Screens.Select.Leaderboards;
using osu.Game.Users; using osu.Game.Users;
@ -42,34 +45,46 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
[Resolved] [Resolved]
private RulesetStore rulesets { get; set; } private RulesetStore rulesets { get; set; }
[Resolved]
private ScoreManager scoreManager { get; set; }
private GetScoresRequest getScoresRequest; private GetScoresRequest getScoresRequest;
private CancellationTokenSource loadCancellationSource;
protected APILegacyScores Scores protected APILegacyScores Scores
{ {
set => Schedule(() => set => Schedule(() =>
{ {
loadCancellationSource?.Cancel();
loadCancellationSource = new CancellationTokenSource();
topScoresContainer.Clear(); topScoresContainer.Clear();
scoreTable.ClearScores();
scoreTable.Hide();
if (value?.Scores.Any() != true) if (value?.Scores.Any() != true)
{
scoreTable.ClearScores();
scoreTable.Hide();
return; return;
}
var scoreInfos = value.Scores.Select(s => s.CreateScoreInfo(rulesets)).ToList(); scoreManager.OrderByTotalScoreAsync(value.Scores.Select(s => s.CreateScoreInfo(rulesets)).ToArray(), loadCancellationSource.Token)
var topScore = scoreInfos.First(); .ContinueWith(ordered => Schedule(() =>
{
if (loadCancellationSource.IsCancellationRequested)
return;
scoreTable.DisplayScores(scoreInfos, topScore.Beatmap?.Status.GrantsPerformancePoints() == true); var topScore = ordered.Result.First();
scoreTable.Show();
var userScore = value.UserScore; scoreTable.DisplayScores(ordered.Result, topScore.Beatmap?.Status.GrantsPerformancePoints() == true);
var userScoreInfo = userScore?.Score.CreateScoreInfo(rulesets); scoreTable.Show();
topScoresContainer.Add(new DrawableTopScore(topScore)); var userScore = value.UserScore;
var userScoreInfo = userScore?.Score.CreateScoreInfo(rulesets);
if (userScoreInfo != null && userScoreInfo.OnlineScoreID != topScore.OnlineScoreID) topScoresContainer.Add(new DrawableTopScore(topScore));
topScoresContainer.Add(new DrawableTopScore(userScoreInfo, userScore.Position));
if (userScoreInfo != null && userScoreInfo.OnlineScoreID != topScore.OnlineScoreID)
topScoresContainer.Add(new DrawableTopScore(userScoreInfo, userScore.Position));
}), TaskContinuationOptions.OnlyOnRanToCompletion);
}); });
} }

View File

@ -4,10 +4,13 @@
using System; using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.MatrixExtensions;
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.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Handlers.Tablet; using osu.Framework.Input.Handlers.Tablet;
using osu.Framework.Utils;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osuTK; using osuTK;
@ -17,6 +20,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
{ {
public class TabletAreaSelection : CompositeDrawable public class TabletAreaSelection : CompositeDrawable
{ {
public bool IsWithinBounds { get; private set; }
private readonly ITabletHandler handler; private readonly ITabletHandler handler;
private Container tabletContainer; private Container tabletContainer;
@ -109,29 +114,30 @@ namespace osu.Game.Overlays.Settings.Sections.Input
areaOffset.BindTo(handler.AreaOffset); areaOffset.BindTo(handler.AreaOffset);
areaOffset.BindValueChanged(val => areaOffset.BindValueChanged(val =>
{ {
usableAreaContainer.MoveTo(val.NewValue, 100, Easing.OutQuint) usableAreaContainer.MoveTo(val.NewValue, 100, Easing.OutQuint);
.OnComplete(_ => checkBounds()); // required as we are using SSDQ. checkBounds();
}, true); }, true);
areaSize.BindTo(handler.AreaSize); areaSize.BindTo(handler.AreaSize);
areaSize.BindValueChanged(val => areaSize.BindValueChanged(val =>
{ {
usableAreaContainer.ResizeTo(val.NewValue, 100, Easing.OutQuint) usableAreaContainer.ResizeTo(val.NewValue, 100, Easing.OutQuint);
.OnComplete(_ => checkBounds()); // required as we are using SSDQ.
int x = (int)val.NewValue.X; int x = (int)val.NewValue.X;
int y = (int)val.NewValue.Y; int y = (int)val.NewValue.Y;
int commonDivider = greatestCommonDivider(x, y); int commonDivider = greatestCommonDivider(x, y);
usableAreaText.Text = $"{(float)x / commonDivider}:{(float)y / commonDivider}"; usableAreaText.Text = $"{(float)x / commonDivider}:{(float)y / commonDivider}";
checkBounds();
}, true); }, true);
rotation.BindTo(handler.Rotation); rotation.BindTo(handler.Rotation);
rotation.BindValueChanged(val => rotation.BindValueChanged(val =>
{ {
usableAreaContainer.RotateTo(val.NewValue, 100, Easing.OutQuint);
tabletContainer.RotateTo(-val.NewValue, 800, Easing.OutQuint); tabletContainer.RotateTo(-val.NewValue, 800, Easing.OutQuint);
usableAreaContainer.RotateTo(val.NewValue, 100, Easing.OutQuint)
.OnComplete(_ => checkBounds()); // required as we are using SSDQ. checkBounds();
}, true); }, true);
tablet.BindTo(handler.Tablet); tablet.BindTo(handler.Tablet);
@ -169,12 +175,35 @@ namespace osu.Game.Overlays.Settings.Sections.Input
if (tablet.Value == null) if (tablet.Value == null)
return; return;
var usableSsdq = usableAreaContainer.ScreenSpaceDrawQuad; // allow for some degree of floating point error, as we don't care about being perfect here.
const float lenience = 0.5f;
bool isWithinBounds = tabletContainer.ScreenSpaceDrawQuad.Contains(usableSsdq.TopLeft + new Vector2(1)) && var tabletArea = new Quad(-lenience, -lenience, tablet.Value.Size.X + lenience * 2, tablet.Value.Size.Y + lenience * 2);
tabletContainer.ScreenSpaceDrawQuad.Contains(usableSsdq.BottomRight - new Vector2(1));
usableFill.FadeColour(isWithinBounds ? colour.Blue : colour.RedLight, 100); var halfUsableArea = areaSize.Value / 2;
var offset = areaOffset.Value;
var usableAreaQuad = new Quad(
new Vector2(-halfUsableArea.X, -halfUsableArea.Y),
new Vector2(halfUsableArea.X, -halfUsableArea.Y),
new Vector2(-halfUsableArea.X, halfUsableArea.Y),
new Vector2(halfUsableArea.X, halfUsableArea.Y)
);
var matrix = Matrix3.Identity;
MatrixExtensions.TranslateFromLeft(ref matrix, offset);
MatrixExtensions.RotateFromLeft(ref matrix, MathUtils.DegreesToRadians(rotation.Value));
usableAreaQuad *= matrix;
IsWithinBounds =
tabletArea.Contains(usableAreaQuad.TopLeft) &&
tabletArea.Contains(usableAreaQuad.TopRight) &&
tabletArea.Contains(usableAreaQuad.BottomLeft) &&
tabletArea.Contains(usableAreaQuad.BottomRight);
usableFill.FadeColour(IsWithinBounds ? colour.Blue : colour.RedLight, 100);
} }
protected override void Update() protected override void Update()

View File

@ -20,6 +20,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
{ {
public class TabletSettings : SettingsSubsection public class TabletSettings : SettingsSubsection
{ {
public TabletAreaSelection AreaSelection { get; private set; }
private readonly ITabletHandler tabletHandler; private readonly ITabletHandler tabletHandler;
private readonly Bindable<bool> enabled = new BindableBool(true); private readonly Bindable<bool> enabled = new BindableBool(true);
@ -121,7 +123,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
Children = new Drawable[] Children = new Drawable[]
{ {
new TabletAreaSelection(tabletHandler) AreaSelection = new TabletAreaSelection(tabletHandler)
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Height = 300, Height = 300,

View File

@ -13,6 +13,7 @@ using Microsoft.EntityFrameworkCore;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Threading;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Database; using osu.Game.Database;
@ -36,6 +37,7 @@ namespace osu.Game.Scoring
private readonly RulesetStore rulesets; private readonly RulesetStore rulesets;
private readonly Func<BeatmapManager> beatmaps; private readonly Func<BeatmapManager> beatmaps;
private readonly Scheduler scheduler;
[CanBeNull] [CanBeNull]
private readonly Func<BeatmapDifficultyCache> difficulties; private readonly Func<BeatmapDifficultyCache> difficulties;
@ -43,12 +45,13 @@ namespace osu.Game.Scoring
[CanBeNull] [CanBeNull]
private readonly OsuConfigManager configManager; private readonly OsuConfigManager configManager;
public ScoreManager(RulesetStore rulesets, Func<BeatmapManager> beatmaps, Storage storage, IAPIProvider api, IDatabaseContextFactory contextFactory, IIpcHost importHost = null, public ScoreManager(RulesetStore rulesets, Func<BeatmapManager> beatmaps, Storage storage, IAPIProvider api, IDatabaseContextFactory contextFactory, Scheduler scheduler,
Func<BeatmapDifficultyCache> difficulties = null, OsuConfigManager configManager = null) IIpcHost importHost = null, Func<BeatmapDifficultyCache> difficulties = null, OsuConfigManager configManager = null)
: base(storage, contextFactory, api, new ScoreStore(contextFactory, storage), importHost) : base(storage, contextFactory, api, new ScoreStore(contextFactory, storage), importHost)
{ {
this.rulesets = rulesets; this.rulesets = rulesets;
this.beatmaps = beatmaps; this.beatmaps = beatmaps;
this.scheduler = scheduler;
this.difficulties = difficulties; this.difficulties = difficulties;
this.configManager = configManager; this.configManager = configManager;
} }
@ -103,6 +106,32 @@ namespace osu.Game.Scoring
=> base.CheckLocalAvailability(model, items) => base.CheckLocalAvailability(model, items)
|| (model.OnlineScoreID != null && items.Any(i => i.OnlineScoreID == model.OnlineScoreID)); || (model.OnlineScoreID != null && items.Any(i => i.OnlineScoreID == model.OnlineScoreID));
/// <summary>
/// Orders an array of <see cref="ScoreInfo"/>s by total score.
/// </summary>
/// <param name="scores">The array of <see cref="ScoreInfo"/>s to reorder.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to cancel the process.</param>
/// <returns>The given <paramref name="scores"/> ordered by decreasing total score.</returns>
public async Task<ScoreInfo[]> OrderByTotalScoreAsync(ScoreInfo[] scores, CancellationToken cancellationToken = default)
{
var difficultyCache = difficulties?.Invoke();
if (difficultyCache != null)
{
// Compute difficulties asynchronously first to prevent blocking via the GetTotalScore() call below.
foreach (var s in scores)
{
await difficultyCache.GetDifficultyAsync(s.Beatmap, s.Ruleset, s.Mods, cancellationToken).ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();
}
}
// We're calling .Result, but this should not be a blocking call due to the above GetDifficultyAsync() calls.
return scores.OrderByDescending(s => GetTotalScoreAsync(s, cancellationToken: cancellationToken).Result)
.ThenBy(s => s.OnlineScoreID)
.ToArray();
}
/// <summary> /// <summary>
/// Retrieves a bindable that represents the total score of a <see cref="ScoreInfo"/>. /// Retrieves a bindable that represents the total score of a <see cref="ScoreInfo"/>.
/// </summary> /// </summary>
@ -111,9 +140,9 @@ namespace osu.Game.Scoring
/// </remarks> /// </remarks>
/// <param name="score">The <see cref="ScoreInfo"/> to retrieve the bindable for.</param> /// <param name="score">The <see cref="ScoreInfo"/> to retrieve the bindable for.</param>
/// <returns>The bindable containing the total score.</returns> /// <returns>The bindable containing the total score.</returns>
public Bindable<long> GetBindableTotalScore(ScoreInfo score) public Bindable<long> GetBindableTotalScore([NotNull] ScoreInfo score)
{ {
var bindable = new TotalScoreBindable(score, difficulties); var bindable = new TotalScoreBindable(score, this);
configManager?.BindWith(OsuSetting.ScoreDisplayMode, bindable.ScoringMode); configManager?.BindWith(OsuSetting.ScoreDisplayMode, bindable.ScoringMode);
return bindable; return bindable;
} }
@ -126,7 +155,83 @@ namespace osu.Game.Scoring
/// </remarks> /// </remarks>
/// <param name="score">The <see cref="ScoreInfo"/> to retrieve the bindable for.</param> /// <param name="score">The <see cref="ScoreInfo"/> to retrieve the bindable for.</param>
/// <returns>The bindable containing the formatted total score string.</returns> /// <returns>The bindable containing the formatted total score string.</returns>
public Bindable<string> GetBindableTotalScoreString(ScoreInfo score) => new TotalScoreStringBindable(GetBindableTotalScore(score)); public Bindable<string> GetBindableTotalScoreString([NotNull] ScoreInfo score) => new TotalScoreStringBindable(GetBindableTotalScore(score));
/// <summary>
/// Retrieves the total score of a <see cref="ScoreInfo"/> in the given <see cref="ScoringMode"/>.
/// The score is returned in a callback that is run on the update thread.
/// </summary>
/// <param name="score">The <see cref="ScoreInfo"/> to calculate the total score of.</param>
/// <param name="callback">The callback to be invoked with the total score.</param>
/// <param name="mode">The <see cref="ScoringMode"/> to return the total score as.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to cancel the process.</param>
public void GetTotalScore([NotNull] ScoreInfo score, [NotNull] Action<long> callback, ScoringMode mode = ScoringMode.Standardised, CancellationToken cancellationToken = default)
{
GetTotalScoreAsync(score, mode, cancellationToken)
.ContinueWith(s => scheduler.Add(() => callback(s.Result)), TaskContinuationOptions.OnlyOnRanToCompletion);
}
/// <summary>
/// Retrieves the total score of a <see cref="ScoreInfo"/> in the given <see cref="ScoringMode"/>.
/// </summary>
/// <param name="score">The <see cref="ScoreInfo"/> to calculate the total score of.</param>
/// <param name="mode">The <see cref="ScoringMode"/> to return the total score as.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to cancel the process.</param>
/// <returns>The total score.</returns>
public async Task<long> GetTotalScoreAsync([NotNull] ScoreInfo score, ScoringMode mode = ScoringMode.Standardised, CancellationToken cancellationToken = default)
{
if (score.Beatmap == null)
return score.TotalScore;
int beatmapMaxCombo;
double accuracy = score.Accuracy;
if (score.IsLegacyScore)
{
if (score.RulesetID == 3)
{
// In osu!stable, a full-GREAT score has 100% accuracy in mania. Along with a full combo, the score becomes indistinguishable from a full-PERFECT score.
// To get around this, recalculate accuracy based on the hit statistics.
// Note: This cannot be applied universally to all legacy scores, as some rulesets (e.g. catch) group multiple judgements together.
double maxBaseScore = score.Statistics.Select(kvp => kvp.Value).Sum() * Judgement.ToNumericResult(HitResult.Perfect);
double baseScore = score.Statistics.Select(kvp => Judgement.ToNumericResult(kvp.Key) * kvp.Value).Sum();
if (maxBaseScore > 0)
accuracy = baseScore / maxBaseScore;
}
// This score is guaranteed to be an osu!stable score.
// The combo must be determined through either the beatmap's max combo value or the difficulty calculator, as lazer's scoring has changed and the score statistics cannot be used.
if (score.Beatmap.MaxCombo != null)
beatmapMaxCombo = score.Beatmap.MaxCombo.Value;
else
{
if (score.Beatmap.ID == 0 || difficulties == null)
{
// We don't have enough information (max combo) to compute the score, so use the provided score.
return score.TotalScore;
}
// We can compute the max combo locally after the async beatmap difficulty computation.
var difficulty = await difficulties().GetDifficultyAsync(score.Beatmap, score.Ruleset, score.Mods, cancellationToken).ConfigureAwait(false);
beatmapMaxCombo = difficulty.MaxCombo;
}
}
else
{
// This is guaranteed to be a non-legacy score.
// The combo must be determined through the score's statistics, as both the beatmap's max combo and the difficulty calculator will provide osu!stable combo values.
beatmapMaxCombo = Enum.GetValues(typeof(HitResult)).OfType<HitResult>().Where(r => r.AffectsCombo()).Select(r => score.Statistics.GetValueOrDefault(r)).Sum();
}
if (beatmapMaxCombo == 0)
return 0;
var ruleset = score.Ruleset.CreateInstance();
var scoreProcessor = ruleset.CreateScoreProcessor();
scoreProcessor.Mods.Value = score.Mods;
return (long)Math.Round(scoreProcessor.GetScore(mode, beatmapMaxCombo, accuracy, (double)score.MaxCombo / beatmapMaxCombo, score.Statistics));
}
/// <summary> /// <summary>
/// Provides the total score of a <see cref="ScoreInfo"/>. Responds to changes in the currently-selected <see cref="ScoringMode"/>. /// Provides the total score of a <see cref="ScoreInfo"/>. Responds to changes in the currently-selected <see cref="ScoringMode"/>.
@ -136,99 +241,29 @@ namespace osu.Game.Scoring
public readonly Bindable<ScoringMode> ScoringMode = new Bindable<ScoringMode>(); public readonly Bindable<ScoringMode> ScoringMode = new Bindable<ScoringMode>();
private readonly ScoreInfo score; private readonly ScoreInfo score;
private readonly Func<BeatmapDifficultyCache> difficulties; private readonly ScoreManager scoreManager;
private CancellationTokenSource difficultyCalculationCancellationSource;
/// <summary> /// <summary>
/// Creates a new <see cref="TotalScoreBindable"/>. /// Creates a new <see cref="TotalScoreBindable"/>.
/// </summary> /// </summary>
/// <param name="score">The <see cref="ScoreInfo"/> to provide the total score of.</param> /// <param name="score">The <see cref="ScoreInfo"/> to provide the total score of.</param>
/// <param name="difficulties">A function to retrieve the <see cref="BeatmapDifficultyCache"/>.</param> /// <param name="scoreManager">The <see cref="ScoreManager"/>.</param>
public TotalScoreBindable(ScoreInfo score, Func<BeatmapDifficultyCache> difficulties) public TotalScoreBindable(ScoreInfo score, ScoreManager scoreManager)
{ {
this.score = score; this.score = score;
this.difficulties = difficulties; this.scoreManager = scoreManager;
ScoringMode.BindValueChanged(onScoringModeChanged, true); ScoringMode.BindValueChanged(onScoringModeChanged, true);
} }
private IBindable<StarDifficulty?> difficultyBindable;
private CancellationTokenSource difficultyCancellationSource;
private void onScoringModeChanged(ValueChangedEvent<ScoringMode> mode) private void onScoringModeChanged(ValueChangedEvent<ScoringMode> mode)
{ {
difficultyCancellationSource?.Cancel(); difficultyCalculationCancellationSource?.Cancel();
difficultyCancellationSource = null; difficultyCalculationCancellationSource = new CancellationTokenSource();
if (score.Beatmap == null) scoreManager.GetTotalScore(score, s => Value = s, mode.NewValue, difficultyCalculationCancellationSource.Token);
{
Value = score.TotalScore;
return;
}
int beatmapMaxCombo;
double accuracy = score.Accuracy;
if (score.IsLegacyScore)
{
if (score.RulesetID == 3)
{
// In osu!stable, a full-GREAT score has 100% accuracy in mania. Along with a full combo, the score becomes indistinguishable from a full-PERFECT score.
// To get around this, recalculate accuracy based on the hit statistics.
// Note: This cannot be applied universally to all legacy scores, as some rulesets (e.g. catch) group multiple judgements together.
double maxBaseScore = score.Statistics.Select(kvp => kvp.Value).Sum() * Judgement.ToNumericResult(HitResult.Perfect);
double baseScore = score.Statistics.Select(kvp => Judgement.ToNumericResult(kvp.Key) * kvp.Value).Sum();
if (maxBaseScore > 0)
accuracy = baseScore / maxBaseScore;
}
// This score is guaranteed to be an osu!stable score.
// The combo must be determined through either the beatmap's max combo value or the difficulty calculator, as lazer's scoring has changed and the score statistics cannot be used.
if (score.Beatmap.MaxCombo == null)
{
if (score.Beatmap.ID == 0 || difficulties == null)
{
// We don't have enough information (max combo) to compute the score, so use the provided score.
Value = score.TotalScore;
return;
}
// We can compute the max combo locally after the async beatmap difficulty computation.
difficultyBindable = difficulties().GetBindableDifficulty(score.Beatmap, score.Ruleset, score.Mods, (difficultyCancellationSource = new CancellationTokenSource()).Token);
difficultyBindable.BindValueChanged(d =>
{
if (d.NewValue is StarDifficulty diff)
updateScore(diff.MaxCombo, accuracy);
}, true);
return;
}
beatmapMaxCombo = score.Beatmap.MaxCombo.Value;
}
else
{
// This is guaranteed to be a non-legacy score.
// The combo must be determined through the score's statistics, as both the beatmap's max combo and the difficulty calculator will provide osu!stable combo values.
beatmapMaxCombo = Enum.GetValues(typeof(HitResult)).OfType<HitResult>().Where(r => r.AffectsCombo()).Select(r => score.Statistics.GetValueOrDefault(r)).Sum();
}
updateScore(beatmapMaxCombo, accuracy);
}
private void updateScore(int beatmapMaxCombo, double accuracy)
{
if (beatmapMaxCombo == 0)
{
Value = 0;
return;
}
var ruleset = score.Ruleset.CreateInstance();
var scoreProcessor = ruleset.CreateScoreProcessor();
scoreProcessor.Mods.Value = score.Mods;
Value = (long)Math.Round(scoreProcessor.GetScore(ScoringMode.Value, beatmapMaxCombo, accuracy, (double)score.MaxCombo / beatmapMaxCombo, score.Statistics));
} }
} }

View File

@ -17,15 +17,21 @@ namespace osu.Game.Screens
Origin = Anchor.Centre; Origin = Anchor.Centre;
} }
public void Push(BackgroundScreen screen) /// <summary>
/// Attempt to push a new background screen to this stack.
/// </summary>
/// <param name="screen">The screen to attempt to push.</param>
/// <returns>Whether the push succeeded. For example, if the existing screen was already of the correct type this will return <c>false</c>.</returns>
public bool Push(BackgroundScreen screen)
{ {
if (screen == null) if (screen == null)
return; return false;
if (EqualityComparer<BackgroundScreen>.Default.Equals((BackgroundScreen)CurrentScreen, screen)) if (EqualityComparer<BackgroundScreen>.Default.Equals((BackgroundScreen)CurrentScreen, screen))
return; return false;
base.Push(screen); base.Push(screen);
return true;
} }
} }
} }

View File

@ -0,0 +1,27 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Screens.Edit.Components.Menus
{
public class DifficultyMenuItem : StatefulMenuItem<bool>
{
public BeatmapInfo Beatmap { get; }
public DifficultyMenuItem(BeatmapInfo beatmapInfo, bool selected, Action<BeatmapInfo> difficultyChangeFunc)
: base(beatmapInfo.Version ?? "(unnamed)", null)
{
Beatmap = beatmapInfo;
State.Value = selected;
if (!selected)
Action.Value = () => difficultyChangeFunc.Invoke(beatmapInfo);
}
public override IconUsage? GetIconForState(bool state) => state ? (IconUsage?)FontAwesome.Solid.Check : null;
}
}

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using JetBrains.Annotations;
using osu.Framework; using osu.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -75,6 +76,9 @@ namespace osu.Game.Screens.Edit
private Container<EditorScreen> screenContainer; private Container<EditorScreen> screenContainer;
[CanBeNull]
private readonly EditorLoader loader;
private EditorScreen currentScreen; private EditorScreen currentScreen;
private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor(); private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor();
@ -101,6 +105,11 @@ namespace osu.Game.Screens.Edit
[Resolved] [Resolved]
private MusicController music { get; set; } private MusicController music { get; set; }
public Editor(EditorLoader loader = null)
{
this.loader = loader;
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours, OsuConfigManager config) private void load(OsuColour colours, OsuConfigManager config)
{ {
@ -489,7 +498,7 @@ namespace osu.Game.Screens.Edit
if (isNewBeatmap || HasUnsavedChanges) if (isNewBeatmap || HasUnsavedChanges)
{ {
dialogOverlay?.Push(new PromptForSaveDialog(confirmExit, confirmExitWithSave)); dialogOverlay?.Push(new PromptForSaveDialog(confirmExit, confirmExitWithSave, cancelExit));
return true; return true;
} }
} }
@ -703,11 +712,38 @@ namespace osu.Game.Screens.Edit
if (RuntimeInfo.IsDesktop) if (RuntimeInfo.IsDesktop)
fileMenuItems.Add(new EditorMenuItem("Export package", MenuItemType.Standard, exportBeatmap)); fileMenuItems.Add(new EditorMenuItem("Export package", MenuItemType.Standard, exportBeatmap));
fileMenuItems.Add(new EditorMenuItemSpacer());
var beatmapSet = beatmapManager.QueryBeatmapSet(bs => bs.ID == Beatmap.Value.BeatmapSetInfo.ID) ?? playableBeatmap.BeatmapInfo.BeatmapSet;
var difficultyItems = new List<MenuItem>();
foreach (var rulesetBeatmaps in beatmapSet.Beatmaps.GroupBy(b => b.RulesetID).OrderBy(group => group.Key))
{
if (difficultyItems.Count > 0)
difficultyItems.Add(new EditorMenuItemSpacer());
foreach (var beatmap in rulesetBeatmaps.OrderBy(b => b.StarDifficulty))
difficultyItems.Add(createDifficultyMenuItem(beatmap));
}
fileMenuItems.Add(new EditorMenuItem("Change difficulty") { Items = difficultyItems });
fileMenuItems.Add(new EditorMenuItemSpacer()); fileMenuItems.Add(new EditorMenuItemSpacer());
fileMenuItems.Add(new EditorMenuItem("Exit", MenuItemType.Standard, this.Exit)); fileMenuItems.Add(new EditorMenuItem("Exit", MenuItemType.Standard, this.Exit));
return fileMenuItems; return fileMenuItems;
} }
private DifficultyMenuItem createDifficultyMenuItem(BeatmapInfo beatmapInfo)
{
bool isCurrentDifficulty = playableBeatmap.BeatmapInfo.Equals(beatmapInfo);
return new DifficultyMenuItem(beatmapInfo, isCurrentDifficulty, switchToDifficulty);
}
private void switchToDifficulty(BeatmapInfo beatmapInfo) => loader?.ScheduleDifficultySwitch(beatmapInfo);
private void cancelExit() => loader?.CancelPendingDifficultySwitch();
public double SnapTime(double time, double? referenceTime) => editorBeatmap.SnapTime(time, referenceTime); public double SnapTime(double time, double? referenceTime) => editorBeatmap.SnapTime(time, referenceTime);
public double GetBeatLengthAtTime(double referenceTime) => editorBeatmap.GetBeatLengthAtTime(referenceTime); public double GetBeatLengthAtTime(double referenceTime) => editorBeatmap.GetBeatLengthAtTime(referenceTime);

View File

@ -0,0 +1,94 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Screens;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Play;
namespace osu.Game.Screens.Edit
{
/// <summary>
/// Transition screen for the editor.
/// Used to avoid backing out to main menu/song select when switching difficulties from within the editor.
/// </summary>
public class EditorLoader : ScreenWithBeatmapBackground
{
public override float BackgroundParallaxAmount => 0.1f;
public override bool AllowBackButton => false;
public override bool HideOverlaysOnEnter => true;
public override bool DisallowExternalBeatmapRulesetChanges => true;
[Resolved]
private BeatmapManager beatmapManager { get; set; }
[CanBeNull]
private ScheduledDelegate scheduledDifficultySwitch;
protected override void LogoArriving(OsuLogo logo, bool resuming)
{
base.LogoArriving(logo, resuming);
if (!resuming)
{
// the push cannot happen in OnEntering() or similar (even if scheduled), because the transition from main menu will look bad.
// that is because this screen pushing the editor makes it no longer current, and OsuScreen checks if the screen is current
// before enqueueing this screen's LogoArriving onto the logo animation sequence.
pushEditor();
}
}
[BackgroundDependencyLoader]
private void load()
{
AddRangeInternal(new Drawable[]
{
new LoadingSpinner(true)
{
State = { Value = Visibility.Visible },
}
});
}
public void ScheduleDifficultySwitch(BeatmapInfo beatmapInfo)
{
scheduledDifficultySwitch?.Cancel();
ValidForResume = true;
this.MakeCurrent();
scheduledDifficultySwitch = Schedule(() =>
{
Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapInfo);
// This screen is a weird exception to the rule that nothing after song select changes the global beatmap.
// Because of this, we need to update the background stack's beatmap to match.
// If we don't do this, the editor will see a discrepancy and create a new background, along with an unnecessary transition.
ApplyToBackground(b => b.Beatmap = Beatmap.Value);
pushEditor();
});
}
private void pushEditor()
{
this.Push(new Editor(this));
ValidForResume = false;
}
public void CancelPendingDifficultySwitch()
{
scheduledDifficultySwitch?.Cancel();
ValidForResume = false;
}
}
}

View File

@ -9,7 +9,7 @@ namespace osu.Game.Screens.Edit
{ {
public class PromptForSaveDialog : PopupDialog public class PromptForSaveDialog : PopupDialog
{ {
public PromptForSaveDialog(Action exit, Action saveAndExit) public PromptForSaveDialog(Action exit, Action saveAndExit, Action cancel)
{ {
HeaderText = "Did you want to save your changes?"; HeaderText = "Did you want to save your changes?";
@ -30,6 +30,7 @@ namespace osu.Game.Screens.Edit
new PopupDialogCancelButton new PopupDialogCancelButton
{ {
Text = @"Oops, continue editing", Text = @"Oops, continue editing",
Action = cancel
}, },
}; };
} }

View File

@ -103,7 +103,7 @@ namespace osu.Game.Screens.Menu
OnEdit = delegate OnEdit = delegate
{ {
Beatmap.SetDefault(); Beatmap.SetDefault();
this.Push(new Editor()); this.Push(new EditorLoader());
}, },
OnSolo = loadSoloSongSelect, OnSolo = loadSoloSongSelect,
OnMultiplayer = () => this.Push(new Multiplayer()), OnMultiplayer = () => this.Push(new Multiplayer()),

View File

@ -32,6 +32,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
[Resolved] [Resolved]
private IAPIProvider api { get; set; } private IAPIProvider api { get; set; }
[Resolved]
private ScoreManager scoreManager { get; set; }
public PlaylistsResultsScreen(ScoreInfo score, long roomId, PlaylistItem playlistItem, bool allowRetry, bool allowWatchingReplay = true) public PlaylistsResultsScreen(ScoreInfo score, long roomId, PlaylistItem playlistItem, bool allowRetry, bool allowWatchingReplay = true)
: base(score, allowRetry, allowWatchingReplay) : base(score, allowRetry, allowWatchingReplay)
{ {
@ -166,23 +169,28 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
/// <param name="pivot">An optional pivot around which the scores were retrieved.</param> /// <param name="pivot">An optional pivot around which the scores were retrieved.</param>
private void performSuccessCallback([NotNull] Action<IEnumerable<ScoreInfo>> callback, [NotNull] List<MultiplayerScore> scores, [CanBeNull] MultiplayerScores pivot = null) private void performSuccessCallback([NotNull] Action<IEnumerable<ScoreInfo>> callback, [NotNull] List<MultiplayerScore> scores, [CanBeNull] MultiplayerScores pivot = null)
{ {
var scoreInfos = new List<ScoreInfo>(scores.Select(s => s.CreateScoreInfo(playlistItem))); var scoreInfos = scores.Select(s => s.CreateScoreInfo(playlistItem)).ToArray();
// Select a score if we don't already have one selected. // Score panels calculate total score before displaying, which can take some time. In order to count that calculation as part of the loading spinner display duration,
// Note: This is done before the callback so that the panel list centres on the selected score before panels are added (eliminating initial scroll). // calculate the total scores locally before invoking the success callback.
if (SelectedScore.Value == null) scoreManager.OrderByTotalScoreAsync(scoreInfos).ContinueWith(_ => Schedule(() =>
{ {
Schedule(() => // Select a score if we don't already have one selected.
// Note: This is done before the callback so that the panel list centres on the selected score before panels are added (eliminating initial scroll).
if (SelectedScore.Value == null)
{ {
// Prefer selecting the local user's score, or otherwise default to the first visible score. Schedule(() =>
SelectedScore.Value = scoreInfos.FirstOrDefault(s => s.User.Id == api.LocalUser.Value.Id) ?? scoreInfos.FirstOrDefault(); {
}); // Prefer selecting the local user's score, or otherwise default to the first visible score.
} SelectedScore.Value = scoreInfos.FirstOrDefault(s => s.User.Id == api.LocalUser.Value.Id) ?? scoreInfos.FirstOrDefault();
});
}
// Invoke callback to add the scores. Exclude the user's current score which was added previously. // Invoke callback to add the scores. Exclude the user's current score which was added previously.
callback.Invoke(scoreInfos.Where(s => s.OnlineScoreID != Score?.OnlineScoreID)); callback.Invoke(scoreInfos.Where(s => s.OnlineScoreID != Score?.OnlineScoreID));
hideLoadingSpinners(pivot); hideLoadingSpinners(pivot);
}));
} }
private void hideLoadingSpinners([CanBeNull] MultiplayerScores pivot = null) private void hideLoadingSpinners([CanBeNull] MultiplayerScores pivot = null)

View File

@ -186,17 +186,14 @@ namespace osu.Game.Screens
{ {
applyArrivingDefaults(false); applyArrivingDefaults(false);
backgroundStack?.Push(ownedBackground = CreateBackground()); if (backgroundStack?.Push(ownedBackground = CreateBackground()) != true)
background = backgroundStack?.CurrentScreen as BackgroundScreen;
if (background != ownedBackground)
{ {
// background may have not been replaced, at which point we don't want to track the background lifetime. // If the constructed instance was not actually pushed to the background stack, we don't want to track it unnecessarily.
ownedBackground?.Dispose(); ownedBackground?.Dispose();
ownedBackground = null; ownedBackground = null;
} }
background = backgroundStack?.CurrentScreen as BackgroundScreen;
base.OnEntering(last); base.OnEntering(last);
} }

View File

@ -52,8 +52,7 @@ namespace osu.Game.Screens.Ranking
private Drawable bottomPanel; private Drawable bottomPanel;
private Container<ScorePanel> detachedPanelContainer; private Container<ScorePanel> detachedPanelContainer;
private bool fetchedInitialScores; private bool lastFetchCompleted;
private APIRequest nextPageRequest;
private readonly bool allowRetry; private readonly bool allowRetry;
private readonly bool allowWatchingReplay; private readonly bool allowWatchingReplay;
@ -191,8 +190,10 @@ namespace osu.Game.Screens.Ranking
{ {
base.Update(); base.Update();
if (fetchedInitialScores && nextPageRequest == null) if (lastFetchCompleted)
{ {
APIRequest nextPageRequest = null;
if (ScorePanelList.IsScrolledToStart) if (ScorePanelList.IsScrolledToStart)
nextPageRequest = FetchNextPage(-1, fetchScoresCallback); nextPageRequest = FetchNextPage(-1, fetchScoresCallback);
else if (ScorePanelList.IsScrolledToEnd) else if (ScorePanelList.IsScrolledToEnd)
@ -200,10 +201,7 @@ namespace osu.Game.Screens.Ranking
if (nextPageRequest != null) if (nextPageRequest != null)
{ {
// Scheduled after children to give the list a chance to update its scroll position and not potentially trigger a second request too early. lastFetchCompleted = false;
nextPageRequest.Success += () => ScheduleAfterChildren(() => nextPageRequest = null);
nextPageRequest.Failure += _ => ScheduleAfterChildren(() => nextPageRequest = null);
api.Queue(nextPageRequest); api.Queue(nextPageRequest);
} }
} }
@ -229,7 +227,7 @@ namespace osu.Game.Screens.Ranking
foreach (var s in scores) foreach (var s in scores)
addScore(s); addScore(s);
fetchedInitialScores = true; lastFetchCompleted = true;
}); });
public override void OnEntering(IScreen last) public override void OnEntering(IScreen last)

View File

@ -5,10 +5,14 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables; 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.Game.Beatmaps;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Scoring; using osu.Game.Scoring;
using osuTK; using osuTK;
@ -36,12 +40,14 @@ namespace osu.Game.Screens.Ranking
/// <summary> /// <summary>
/// Whether this <see cref="ScorePanelList"/> can be scrolled and is currently scrolled to the start. /// Whether this <see cref="ScorePanelList"/> can be scrolled and is currently scrolled to the start.
/// </summary> /// </summary>
public bool IsScrolledToStart => flow.Count > 0 && scroll.ScrollableExtent > 0 && scroll.Current <= scroll_endpoint_distance; public bool IsScrolledToStart => flow.Count > 0 && AllPanelsVisible && scroll.ScrollableExtent > 0 && scroll.Current <= scroll_endpoint_distance;
/// <summary> /// <summary>
/// Whether this <see cref="ScorePanelList"/> can be scrolled and is currently scrolled to the end. /// Whether this <see cref="ScorePanelList"/> can be scrolled and is currently scrolled to the end.
/// </summary> /// </summary>
public bool IsScrolledToEnd => flow.Count > 0 && scroll.ScrollableExtent > 0 && scroll.IsScrolledToEnd(scroll_endpoint_distance); public bool IsScrolledToEnd => flow.Count > 0 && AllPanelsVisible && scroll.ScrollableExtent > 0 && scroll.IsScrolledToEnd(scroll_endpoint_distance);
public bool AllPanelsVisible => flow.All(p => p.IsPresent);
/// <summary> /// <summary>
/// The current scroll position. /// The current scroll position.
@ -60,6 +66,13 @@ namespace osu.Game.Screens.Ranking
public readonly Bindable<ScoreInfo> SelectedScore = new Bindable<ScoreInfo>(); public readonly Bindable<ScoreInfo> SelectedScore = new Bindable<ScoreInfo>();
[Resolved]
private ScoreManager scoreManager { get; set; }
[Resolved]
private BeatmapDifficultyCache difficultyCache { get; set; }
private readonly CancellationTokenSource loadCancellationSource = new CancellationTokenSource();
private readonly Flow flow; private readonly Flow flow;
private readonly Scroll scroll; private readonly Scroll scroll;
private ScorePanel expandedPanel; private ScorePanel expandedPanel;
@ -90,6 +103,9 @@ namespace osu.Game.Screens.Ranking
{ {
base.LoadComplete(); base.LoadComplete();
foreach (var d in flow)
displayScore(d);
SelectedScore.BindValueChanged(selectedScoreChanged, true); SelectedScore.BindValueChanged(selectedScoreChanged, true);
} }
@ -114,36 +130,56 @@ namespace osu.Game.Screens.Ranking
}; };
}); });
flow.Add(panel.CreateTrackingContainer().With(d => var trackingContainer = panel.CreateTrackingContainer().With(d =>
{ {
d.Anchor = Anchor.Centre; d.Anchor = Anchor.Centre;
d.Origin = Anchor.Centre; d.Origin = Anchor.Centre;
})); d.Hide();
});
flow.Add(trackingContainer);
if (IsLoaded) if (IsLoaded)
{ displayScore(trackingContainer);
if (SelectedScore.Value == score)
{
SelectedScore.TriggerChange();
}
else
{
// We want the scroll position to remain relative to the expanded panel. When a new panel is added after the expanded panel, nothing needs to be done.
// But when a panel is added before the expanded panel, we need to offset the scroll position by the width of the new panel.
if (expandedPanel != null && flow.GetPanelIndex(score) < flow.GetPanelIndex(expandedPanel.Score))
{
// A somewhat hacky property is used here because we need to:
// 1) Scroll after the scroll container's visible range is updated.
// 2) Scroll before the scroll container's scroll position is updated.
// Without this, we would have a 1-frame positioning error which looks very jarring.
scroll.InstantScrollTarget = (scroll.InstantScrollTarget ?? scroll.Target) + ScorePanel.CONTRACTED_WIDTH + panel_spacing;
}
}
}
return panel; return panel;
} }
private void displayScore(ScorePanelTrackingContainer trackingContainer)
{
if (!IsLoaded)
return;
var score = trackingContainer.Panel.Score;
// Calculating score can take a while in extreme scenarios, so only display scores after the process completes.
scoreManager.GetTotalScoreAsync(score)
.ContinueWith(totalScore => Schedule(() =>
{
flow.SetLayoutPosition(trackingContainer, totalScore.Result);
trackingContainer.Show();
if (SelectedScore.Value == score)
{
SelectedScore.TriggerChange();
}
else
{
// We want the scroll position to remain relative to the expanded panel. When a new panel is added after the expanded panel, nothing needs to be done.
// But when a panel is added before the expanded panel, we need to offset the scroll position by the width of the new panel.
if (expandedPanel != null && flow.GetPanelIndex(score) < flow.GetPanelIndex(expandedPanel.Score))
{
// A somewhat hacky property is used here because we need to:
// 1) Scroll after the scroll container's visible range is updated.
// 2) Scroll before the scroll container's scroll position is updated.
// Without this, we would have a 1-frame positioning error which looks very jarring.
scroll.InstantScrollTarget = (scroll.InstantScrollTarget ?? scroll.Target) + ScorePanel.CONTRACTED_WIDTH + panel_spacing;
}
}
}), TaskContinuationOptions.OnlyOnRanToCompletion);
}
/// <summary> /// <summary>
/// Brings a <see cref="ScoreInfo"/> to the centre of the screen and expands it. /// Brings a <see cref="ScoreInfo"/> to the centre of the screen and expands it.
/// </summary> /// </summary>
@ -267,6 +303,9 @@ namespace osu.Game.Screens.Ranking
protected override bool OnKeyDown(KeyDownEvent e) protected override bool OnKeyDown(KeyDownEvent e)
{ {
if (expandedPanel == null)
return base.OnKeyDown(e);
var expandedPanelIndex = flow.GetPanelIndex(expandedPanel.Score); var expandedPanelIndex = flow.GetPanelIndex(expandedPanel.Score);
switch (e.Key) switch (e.Key)
@ -285,6 +324,12 @@ namespace osu.Game.Screens.Ranking
return base.OnKeyDown(e); return base.OnKeyDown(e);
} }
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
loadCancellationSource?.Cancel();
}
private class Flow : FillFlowContainer<ScorePanelTrackingContainer> private class Flow : FillFlowContainer<ScorePanelTrackingContainer>
{ {
public override IEnumerable<Drawable> FlowingChildren => applySorting(AliveInternalChildren); public override IEnumerable<Drawable> FlowingChildren => applySorting(AliveInternalChildren);
@ -292,24 +337,8 @@ namespace osu.Game.Screens.Ranking
public int GetPanelIndex(ScoreInfo score) => applySorting(Children).TakeWhile(s => s.Panel.Score != score).Count(); public int GetPanelIndex(ScoreInfo score) => applySorting(Children).TakeWhile(s => s.Panel.Score != score).Count();
private IEnumerable<ScorePanelTrackingContainer> applySorting(IEnumerable<Drawable> drawables) => drawables.OfType<ScorePanelTrackingContainer>() private IEnumerable<ScorePanelTrackingContainer> applySorting(IEnumerable<Drawable> drawables) => drawables.OfType<ScorePanelTrackingContainer>()
.OrderByDescending(s => s.Panel.Score.TotalScore) .OrderByDescending(GetLayoutPosition)
.ThenBy(s => s.Panel.Score.OnlineScoreID); .ThenBy(s => s.Panel.Score.OnlineScoreID);
protected override int Compare(Drawable x, Drawable y)
{
var tX = (ScorePanelTrackingContainer)x;
var tY = (ScorePanelTrackingContainer)y;
int result = tY.Panel.Score.TotalScore.CompareTo(tX.Panel.Score.TotalScore);
if (result != 0)
return result;
if (tX.Panel.Score.OnlineScoreID == null || tY.Panel.Score.OnlineScoreID == null)
return base.Compare(x, y);
return tX.Panel.Score.OnlineScoreID.Value.CompareTo(tY.Panel.Score.OnlineScoreID.Value);
}
} }
private class Scroll : OsuScrollContainer private class Scroll : OsuScrollContainer

View File

@ -4,6 +4,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -66,6 +68,9 @@ namespace osu.Game.Screens.Select.Leaderboards
[Resolved] [Resolved]
private ScoreManager scoreManager { get; set; } private ScoreManager scoreManager { get; set; }
[Resolved]
private BeatmapDifficultyCache difficultyCache { get; set; }
[Resolved] [Resolved]
private IBindable<RulesetInfo> ruleset { get; set; } private IBindable<RulesetInfo> ruleset { get; set; }
@ -120,8 +125,13 @@ namespace osu.Game.Screens.Select.Leaderboards
protected override bool IsOnlineScope => Scope != BeatmapLeaderboardScope.Local; protected override bool IsOnlineScope => Scope != BeatmapLeaderboardScope.Local;
private CancellationTokenSource loadCancellationSource;
protected override APIRequest FetchScores(Action<IEnumerable<ScoreInfo>> scoresCallback) protected override APIRequest FetchScores(Action<IEnumerable<ScoreInfo>> scoresCallback)
{ {
loadCancellationSource?.Cancel();
loadCancellationSource = new CancellationTokenSource();
if (Beatmap == null) if (Beatmap == null)
{ {
PlaceholderState = PlaceholderState.NoneSelected; PlaceholderState = PlaceholderState.NoneSelected;
@ -146,8 +156,8 @@ namespace osu.Game.Screens.Select.Leaderboards
scores = scores.Where(s => s.Mods.Any(m => selectedMods.Contains(m.Acronym))); scores = scores.Where(s => s.Mods.Any(m => selectedMods.Contains(m.Acronym)));
} }
Scores = scores.OrderByDescending(s => s.TotalScore).ToArray(); scoreManager.OrderByTotalScoreAsync(scores.ToArray(), loadCancellationSource.Token)
PlaceholderState = Scores.Any() ? PlaceholderState.Successful : PlaceholderState.NoScores; .ContinueWith(ordered => scoresCallback?.Invoke(ordered.Result), TaskContinuationOptions.OnlyOnRanToCompletion);
return null; return null;
} }
@ -182,8 +192,15 @@ namespace osu.Game.Screens.Select.Leaderboards
req.Success += r => req.Success += r =>
{ {
scoresCallback?.Invoke(r.Scores.Select(s => s.CreateScoreInfo(rulesets))); scoreManager.OrderByTotalScoreAsync(r.Scores.Select(s => s.CreateScoreInfo(rulesets)).ToArray(), loadCancellationSource.Token)
TopScore = r.UserScore?.CreateScoreInfo(rulesets); .ContinueWith(ordered => Schedule(() =>
{
if (loadCancellationSource.IsCancellationRequested)
return;
scoresCallback?.Invoke(ordered.Result);
TopScore = r.UserScore?.CreateScoreInfo(rulesets);
}), TaskContinuationOptions.OnlyOnRanToCompletion);
}; };
return req; return req;

View File

@ -349,7 +349,7 @@ namespace osu.Game.Screens.Select
throw new InvalidOperationException($"Attempted to edit when {nameof(AllowEditing)} is disabled"); throw new InvalidOperationException($"Attempted to edit when {nameof(AllowEditing)} is disabled");
Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap ?? beatmapNoDebounce); Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap ?? beatmapNoDebounce);
this.Push(new Editor()); this.Push(new EditorLoader());
} }
/// <summary> /// <summary>

View File

@ -367,6 +367,11 @@ namespace osu.Game.Tests.Visual
Add(runner = new TestSceneTestRunner.TestRunner()); Add(runner = new TestSceneTestRunner.TestRunner());
} }
protected override void InitialiseFonts()
{
// skip fonts load as it's not required for testing purposes.
}
public void RunTestBlocking(TestScene test) => runner.RunTestBlocking(test); public void RunTestBlocking(TestScene test) => runner.RunTestBlocking(test);
} }
} }

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.3.0" /> <PackageReference Include="Realm" Version="10.3.0" />
<PackageReference Include="ppy.osu.Framework" Version="2021.830.0" /> <PackageReference Include="ppy.osu.Framework" Version="2021.907.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.827.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.907.0" />
<PackageReference Include="Sentry" Version="3.9.0" /> <PackageReference Include="Sentry" Version="3.9.0" />
<PackageReference Include="SharpCompress" Version="0.28.3" /> <PackageReference Include="SharpCompress" Version="0.28.3" />
<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.830.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2021.907.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.827.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.907.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.830.0" /> <PackageReference Include="ppy.osu.Framework" Version="2021.907.0" />
<PackageReference Include="SharpCompress" Version="0.28.3" /> <PackageReference Include="SharpCompress" Version="0.28.3" />
<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" />