diff --git a/.github/workflows/test-diffcalc.yml b/.github/workflows/test-diffcalc.yml
new file mode 100644
index 0000000000..4274d01bab
--- /dev/null
+++ b/.github/workflows/test-diffcalc.yml
@@ -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
\ No newline at end of file
diff --git a/osu.Android.props b/osu.Android.props
index 8a9bf1b9cd..7378450c38 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,8 +51,8 @@
-
-
+
+
diff --git a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs
index 642ecf00b8..8be74f1a7c 100644
--- a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs
+++ b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs
@@ -11,6 +11,7 @@ using osu.Framework.Platform;
using osu.Game.Database;
using osu.Game.Input;
using osu.Game.Input.Bindings;
+using osu.Game.Rulesets;
using Realms;
namespace osu.Game.Tests.Database
@@ -42,7 +43,7 @@ namespace osu.Game.Tests.Database
KeyBindingContainer testContainer = new TestKeyBindingContainer();
- keyBindingStore.Register(testContainer);
+ keyBindingStore.Register(testContainer, Enumerable.Empty());
Assert.That(queryCount(), Is.EqualTo(3));
@@ -66,7 +67,7 @@ namespace osu.Game.Tests.Database
{
KeyBindingContainer testContainer = new TestKeyBindingContainer();
- keyBindingStore.Register(testContainer);
+ keyBindingStore.Register(testContainer, Enumerable.Empty());
using (var primaryUsage = realmContextFactory.GetForRead())
{
diff --git a/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs b/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs
index e45b8f7dc5..785f31386d 100644
--- a/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs
+++ b/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs
@@ -40,10 +40,10 @@ namespace osu.Game.Tests.NonVisual.Skinning
assertPlaybackPosition(0);
AddStep("set start time to 1000", () => animationTimeReference.AnimationStartTime.Value = 1000);
- assertPlaybackPosition(-1000);
+ assertPlaybackPosition(0);
AddStep("set current time to 500", () => animationTimeReference.ManualClock.CurrentTime = 500);
- assertPlaybackPosition(-500);
+ assertPlaybackPosition(0);
}
private void assertPlaybackPosition(double expectedPosition)
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs b/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs
new file mode 100644
index 0000000000..0f3d413a7d
--- /dev/null
+++ b/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs
@@ -0,0 +1,170 @@
+// Copyright (c) ppy Pty Ltd . 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().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().Single();
+ return saveDialog != null;
+ });
+ AddStep("continue editing", () =>
+ {
+ var continueButton = saveDialog.ChildrenOfType().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().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().Single();
+ return saveDialog != null;
+ });
+ AddStep("discard changes", () =>
+ {
+ var continueButton = saveDialog.ChildrenOfType().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 difficulty)
+ {
+ AddUntilStep("wait for menubar to load", () => editor.ChildrenOfType().Any());
+ AddStep("open file menu", () =>
+ {
+ var menuBar = editor.ChildrenOfType().Single();
+ var fileMenu = menuBar.ChildrenOfType().First();
+ InputManager.MoveMouseTo(fileMenu);
+ InputManager.Click(MouseButton.Left);
+ });
+
+ AddStep("open difficulty menu", () =>
+ {
+ var difficultySelector =
+ editor.ChildrenOfType().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()
+ .Last(item => item.Item is DifficultyMenuItem difficultyItem && difficultyItem.Beatmap.Equals(difficulty.Invoke()));
+ InputManager.MoveMouseTo(difficultyMenuItem);
+ InputManager.Click(MouseButton.Left);
+ });
+ }
+
+ private void confirmEditingBeatmap(Func targetDifficulty)
+ {
+ AddUntilStep("current beatmap is correct", () => Beatmap.Value.BeatmapInfo.Equals(targetDifficulty.Invoke()));
+ AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs
index 61d49e4018..4bcc887b9f 100644
--- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs
+++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs
@@ -160,11 +160,14 @@ namespace osu.Game.Tests.Visual.Playlists
Ruleset = { Value = new OsuRuleset().RulesetInfo }
}));
});
+
+ AddUntilStep("wait for load", () => resultsScreen.ChildrenOfType().FirstOrDefault()?.AllPanelsVisible == true);
}
private void waitForDisplay()
{
AddUntilStep("wait for request to complete", () => requestComplete);
+ AddUntilStep("wait for panels to be visible", () => resultsScreen.ChildrenOfType().FirstOrDefault()?.AllPanelsVisible == true);
AddWaitStep("wait for display", 5);
}
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
index ba6b6bd529..631455b727 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
@@ -99,7 +99,7 @@ namespace osu.Game.Tests.Visual.Ranking
TestResultsScreen screen = null;
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
- AddUntilStep("wait for loaded", () => screen.IsLoaded);
+ AddUntilStep("wait for load", () => this.ChildrenOfType().Single().AllPanelsVisible);
AddStep("click expanded panel", () =>
{
@@ -138,7 +138,7 @@ namespace osu.Game.Tests.Visual.Ranking
TestResultsScreen screen = null;
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
- AddUntilStep("wait for loaded", () => screen.IsLoaded);
+ AddUntilStep("wait for load", () => this.ChildrenOfType().Single().AllPanelsVisible);
AddStep("click expanded panel", () =>
{
@@ -177,7 +177,7 @@ namespace osu.Game.Tests.Visual.Ranking
TestResultsScreen screen = null;
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
- AddUntilStep("wait for loaded", () => screen.IsLoaded);
+ AddUntilStep("wait for load", () => this.ChildrenOfType().Single().AllPanelsVisible);
ScorePanel expandedPanel = null;
ScorePanel contractedPanel = null;
@@ -223,6 +223,7 @@ namespace osu.Game.Tests.Visual.Ranking
TestResultsScreen screen = null;
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
+ AddUntilStep("wait for load", () => this.ChildrenOfType().Single().AllPanelsVisible);
AddAssert("download button is disabled", () => !screen.ChildrenOfType().Last().Enabled.Value);
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs
index e65dcb19b1..b2585c0509 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs
@@ -159,6 +159,9 @@ namespace osu.Game.Tests.Visual.Ranking
var firstScore = new TestScoreInfo(new OsuRuleset().RulesetInfo);
var secondScore = new TestScoreInfo(new OsuRuleset().RulesetInfo);
+ firstScore.User.Username = "A";
+ secondScore.User.Username = "B";
+
createListStep(() => new ScorePanelList());
AddStep("add scores and select first", () =>
@@ -168,6 +171,8 @@ namespace osu.Game.Tests.Visual.Ranking
list.SelectedScore.Value = firstScore;
});
+ AddUntilStep("wait for load", () => list.AllPanelsVisible);
+
assertScoreState(firstScore, true);
assertScoreState(secondScore, false);
@@ -182,6 +187,22 @@ namespace osu.Game.Tests.Visual.Ranking
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 creationFunc)
{
AddStep("create list", () => Child = list = creationFunc().With(d =>
diff --git a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs
index da474a64ba..997eac709d 100644
--- a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs
+++ b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs
@@ -1,14 +1,15 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Linq;
using NUnit.Framework;
-using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Input.Handlers.Tablet;
-using osu.Framework.Platform;
+using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Overlays;
+using osu.Game.Overlays.Settings;
using osu.Game.Overlays.Settings.Sections.Input;
using osuTK;
@@ -17,22 +18,34 @@ namespace osu.Game.Tests.Visual.Settings
[TestFixture]
public class TestSceneTabletSettings : OsuTestScene
{
- [BackgroundDependencyLoader]
- private void load(GameHost host)
- {
- var tabletHandler = new TestTabletHandler();
+ private TestTabletHandler tabletHandler;
+ private TabletSettings settings;
- AddRange(new Drawable[]
+ [SetUpSteps]
+ public void SetUpSteps()
+ {
+ AddStep("create settings", () =>
{
- new TabletSettings(tabletHandler)
+ tabletHandler = new TestTabletHandler();
+
+ Children = new Drawable[]
{
- RelativeSizeAxes = Axes.None,
- Width = SettingsPanel.PANEL_WIDTH,
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- }
+ settings = new TabletSettings(tabletHandler)
+ {
+ RelativeSizeAxes = Axes.None,
+ 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 square tablet", () => tabletHandler.SetTabletSize(new Vector2(300, 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));
}
+ [Test]
+ public void TestWideAspectRatioValidity()
+ {
+ AddStep("Test with wide tablet", () => tabletHandler.SetTabletSize(new Vector2(160, 100)));
+
+ AddStep("Reset to full area", () => settings.ChildrenOfType().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 Bindable AreaOffset { get; } = new Bindable();
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs
index 184a2e59da..29815ce9ff 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs
@@ -41,7 +41,7 @@ namespace osu.Game.Tests.Visual.SongSelect
dependencies.Cache(rulesetStore = new RulesetStore(ContextFactory));
dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesetStore, null, dependencies.Get(), Resources, dependencies.Get(), 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;
}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs
index abd1baf0ac..008d91f649 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
+using Humanizer;
using NUnit.Framework;
using osu.Framework.Allocation;
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.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.Category.BindValueChanged(c => category.Text = $"Category: {c.NewValue}", true);
control.Genre.BindValueChanged(g => genre.Text = $"Genre: {g.NewValue}", true);
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs
index 3f9e0048dd..2e30ed9827 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs
@@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.UserInterface
private BeatmapManager beatmapManager;
private ScoreManager scoreManager;
- private readonly List scores = new List();
+ private readonly List importedScores = new List();
private BeatmapInfo beatmap;
[Cached]
@@ -82,7 +82,7 @@ namespace osu.Game.Tests.Visual.UserInterface
dependencies.Cache(rulesetStore = new RulesetStore(ContextFactory));
dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesetStore, null, dependencies.Get(), Resources, dependencies.Get(), 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];
@@ -100,11 +100,9 @@ namespace osu.Game.Tests.Visual.UserInterface
User = new User { Username = "TestUser" },
};
- scores.Add(scoreManager.Import(score).Result);
+ importedScores.Add(scoreManager.Import(score).Result);
}
- scores.Sort(Comparer.Create((s1, s2) => s2.TotalScore.CompareTo(s1.TotalScore)));
-
return dependencies;
}
@@ -134,9 +132,14 @@ namespace osu.Game.Tests.Visual.UserInterface
[Test]
public void TestDeleteViaRightClick()
{
+ ScoreInfo scoreBeingDeleted = null;
AddStep("open menu for top score", () =>
{
- InputManager.MoveMouseTo(leaderboard.ChildrenOfType().First());
+ var leaderboardScore = leaderboard.ChildrenOfType().First();
+
+ scoreBeingDeleted = leaderboardScore.Score;
+
+ InputManager.MoveMouseTo(leaderboardScore);
InputManager.Click(MouseButton.Right);
});
@@ -158,14 +161,14 @@ namespace osu.Game.Tests.Visual.UserInterface
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]
public void TestDeleteViaDatabase()
{
- AddStep("delete top score", () => scoreManager.Delete(scores[0]));
- AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineScoreID != scores[0].OnlineScoreID));
+ AddStep("delete top score", () => scoreManager.Delete(importedScores[0]));
+ AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineScoreID != importedScores[0].OnlineScoreID));
}
}
}
diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs
index 9089169877..03cb4031ca 100644
--- a/osu.Game/Input/RealmKeyBindingStore.cs
+++ b/osu.Game/Input/RealmKeyBindingStore.cs
@@ -46,52 +46,53 @@ namespace osu.Game.Input
}
///
- /// Register a new type of , adding default bindings from .
+ /// Register all defaults for this store.
///
/// The container to populate defaults from.
- public void Register(KeyBindingContainer container) => insertDefaults(container.DefaultKeyBindings);
-
- ///
- /// Register a ruleset, adding default bindings for each of its variants.
- ///
- /// The ruleset to populate defaults from.
- 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 defaults, int? rulesetId = null, int? variant = null)
+ /// The rulesets to populate defaults from.
+ public void Register(KeyBindingContainer container, IEnumerable rulesets)
{
using (var usage = realmFactory.GetForWrite())
{
- // compare counts in database vs defaults
- foreach (var defaultsForAction in defaults.GroupBy(k => k.Action))
+ // intentionally flattened to a list rather than querying against the IQueryable, as nullable fields being queried against aren't indexed.
+ // this is much faster as a result.
+ var existingBindings = usage.Realm.All().ToList();
+
+ insertDefaults(usage, existingBindings, container.DefaultKeyBindings);
+
+ foreach (var ruleset in rulesets)
{
- int existingCount = usage.Realm.All().Count(k => k.RulesetID == rulesetId && k.Variant == variant && k.ActionInt == (int)defaultsForAction.Key);
-
- if (defaultsForAction.Count() <= existingCount)
- continue;
-
- foreach (var k in defaultsForAction.Skip(existingCount))
- {
- // insert any defaults which are missing.
- usage.Realm.Add(new RealmKeyBinding
- {
- KeyCombinationString = k.KeyCombination.ToString(),
- ActionInt = (int)k.Action,
- RulesetID = rulesetId,
- Variant = variant
- });
- }
+ var instance = ruleset.CreateInstance();
+ foreach (var variant in instance.AvailableVariants)
+ insertDefaults(usage, existingBindings, instance.GetDefaultKeyBindings(variant), ruleset.ID, variant);
}
usage.Commit();
}
}
+ private void insertDefaults(RealmContextFactory.RealmUsage usage, List existingBindings, IEnumerable 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
+ }));
+ }
+ }
+
///
/// Keys which should not be allowed for gameplay input purposes.
///
diff --git a/osu.Game/Online/API/Requests/GetUserRequest.cs b/osu.Game/Online/API/Requests/GetUserRequest.cs
index 281926c096..e49c4ab298 100644
--- a/osu.Game/Online/API/Requests/GetUserRequest.cs
+++ b/osu.Game/Online/API/Requests/GetUserRequest.cs
@@ -8,8 +8,9 @@ namespace osu.Game.Online.API.Requests
{
public class GetUserRequest : APIRequest
{
- private readonly string userIdentifier;
+ private readonly string lookup;
public readonly RulesetInfo Ruleset;
+ private readonly LookupType lookupType;
///
/// Gets the currently logged-in user.
@@ -25,7 +26,8 @@ namespace osu.Game.Online.API.Requests
/// The ruleset to get the user's info for.
public GetUserRequest(long? userId = null, RulesetInfo ruleset = null)
{
- this.userIdentifier = userId.ToString();
+ lookup = userId.ToString();
+ lookupType = LookupType.Id;
Ruleset = ruleset;
}
@@ -36,10 +38,17 @@ namespace osu.Game.Online.API.Requests
/// The ruleset to get the user's info for.
public GetUserRequest(string username = null, RulesetInfo ruleset = null)
{
- this.userIdentifier = username;
+ lookup = username;
+ lookupType = LookupType.Username;
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
+ }
}
}
diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs
index 8ce495e274..ae082ca82e 100644
--- a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs
+++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs
@@ -3,6 +3,7 @@
using System.Collections.Generic;
using System.Linq;
+using Humanizer;
using JetBrains.Annotations;
using osu.Framework.IO.Network;
using osu.Game.Extensions;
@@ -83,7 +84,7 @@ namespace osu.Game.Online.API.Requests
req.AddParameter("q", query);
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)
req.AddParameter("m", ruleset.ID.Value.ToString());
diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs
index 934b905a1a..26749a23f9 100644
--- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs
+++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs
@@ -34,6 +34,8 @@ namespace osu.Game.Online.Leaderboards
{
public const float HEIGHT = 60;
+ public readonly ScoreInfo Score;
+
private const float corner_radius = 5;
private const float edge_margin = 5;
private const float background_alpha = 0.25f;
@@ -41,7 +43,6 @@ namespace osu.Game.Online.Leaderboards
protected Container RankContainer { get; private set; }
- private readonly ScoreInfo score;
private readonly int? rank;
private readonly bool allowHighlight;
@@ -67,7 +68,8 @@ namespace osu.Game.Online.Leaderboards
public LeaderboardScore(ScoreInfo score, int? rank, bool allowHighlight = true)
{
- this.score = score;
+ Score = score;
+
this.rank = rank;
this.allowHighlight = allowHighlight;
@@ -78,9 +80,9 @@ namespace osu.Game.Online.Leaderboards
[BackgroundDependencyLoader]
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;
@@ -198,7 +200,7 @@ namespace osu.Game.Online.Leaderboards
{
TextColour = Color4.White,
GlowColour = Color4Extensions.FromHex(@"83ccfa"),
- Current = scoreManager.GetBindableTotalScoreString(score),
+ Current = scoreManager.GetBindableTotalScoreString(Score),
Font = OsuFont.Numeric.With(size: 23),
},
RankContainer = new Container
@@ -206,7 +208,7 @@ namespace osu.Game.Online.Leaderboards
Size = new Vector2(40f, 20f),
Children = new[]
{
- scoreRank = new UpdateableRank(score.Rank)
+ scoreRank = new UpdateableRank(Score.Rank)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -223,7 +225,7 @@ namespace osu.Game.Online.Leaderboards
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
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
-
-
+
+
@@ -93,7 +93,7 @@
-
+