mirror of
https://github.com/ppy/osu.git
synced 2025-03-23 02:57:25 +08:00
Merge branch 'master' into chat-command
This commit is contained in:
commit
f209222812
163
.github/workflows/test-diffcalc.yml
vendored
Normal file
163
.github/workflows/test-diffcalc.yml
vendored
Normal 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
|
@ -51,8 +51,8 @@
|
||||
<Reference Include="Java.Interop" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.827.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.830.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.907.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.907.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Transitive Dependencies">
|
||||
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
|
||||
|
@ -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<RulesetInfo>());
|
||||
|
||||
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<RulesetInfo>());
|
||||
|
||||
using (var primaryUsage = realmContextFactory.GetForRead())
|
||||
{
|
||||
|
@ -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)
|
||||
|
170
osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs
Normal file
170
osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -160,11 +160,14 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
Ruleset = { Value = new OsuRuleset().RulesetInfo }
|
||||
}));
|
||||
});
|
||||
|
||||
AddUntilStep("wait for load", () => resultsScreen.ChildrenOfType<ScorePanelList>().FirstOrDefault()?.AllPanelsVisible == true);
|
||||
}
|
||||
|
||||
private void waitForDisplay()
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -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<ScorePanelList>().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<ScorePanelList>().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<ScorePanelList>().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<ScorePanelList>().Single().AllPanelsVisible);
|
||||
|
||||
AddAssert("download button is disabled", () => !screen.ChildrenOfType<DownloadButton>().Last().Enabled.Value);
|
||||
|
||||
|
@ -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<ScorePanelList> creationFunc)
|
||||
{
|
||||
AddStep("create list", () => Child = list = creationFunc().With(d =>
|
||||
|
@ -1,14 +1,15 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.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<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 Bindable<Vector2> AreaOffset { get; } = new Bindable<Vector2>();
|
||||
|
@ -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<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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
private BeatmapManager beatmapManager;
|
||||
private ScoreManager scoreManager;
|
||||
|
||||
private readonly List<ScoreInfo> scores = new List<ScoreInfo>();
|
||||
private readonly List<ScoreInfo> importedScores = new List<ScoreInfo>();
|
||||
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<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];
|
||||
|
||||
@ -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<ScoreInfo>.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<LeaderboardScore>().First());
|
||||
var leaderboardScore = leaderboard.ChildrenOfType<LeaderboardScore>().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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -46,52 +46,53 @@ namespace osu.Game.Input
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// <param name="container">The container to populate defaults from.</param>
|
||||
public void Register(KeyBindingContainer container) => insertDefaults(container.DefaultKeyBindings);
|
||||
|
||||
/// <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)
|
||||
/// <param name="rulesets">The rulesets to populate defaults from.</param>
|
||||
public void Register(KeyBindingContainer container, IEnumerable<RulesetInfo> 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<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);
|
||||
|
||||
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<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>
|
||||
/// Keys which should not be allowed for gameplay input purposes.
|
||||
/// </summary>
|
||||
|
@ -8,8 +8,9 @@ namespace osu.Game.Online.API.Requests
|
||||
{
|
||||
public class GetUserRequest : APIRequest<User>
|
||||
{
|
||||
private readonly string userIdentifier;
|
||||
private readonly string lookup;
|
||||
public readonly RulesetInfo Ruleset;
|
||||
private readonly LookupType lookupType;
|
||||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
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
|
||||
/// <param name="ruleset">The ruleset to get the user's info for.</param>
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
|
@ -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<MenuItem> items = new List<MenuItem>();
|
||||
|
||||
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));
|
||||
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));
|
||||
|
||||
if (score.Files?.Count > 0)
|
||||
items.Add(new OsuMenuItem("Export", MenuItemType.Standard, () => scoreManager.Export(score)));
|
||||
if (Score.Files?.Count > 0)
|
||||
items.Add(new OsuMenuItem("Export", MenuItemType.Standard, () => scoreManager.Export(Score)));
|
||||
|
||||
if (score.ID != 0)
|
||||
items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(score))));
|
||||
if (Score.ID != 0)
|
||||
items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(Score))));
|
||||
|
||||
return items.ToArray();
|
||||
}
|
||||
|
@ -205,31 +205,7 @@ namespace osu.Game
|
||||
dependencies.CacheAs(this);
|
||||
dependencies.CacheAs(LocalConfig);
|
||||
|
||||
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");
|
||||
InitialiseFonts();
|
||||
|
||||
Audio.Samples.PlaybackConcurrency = SAMPLE_CONCURRENCY;
|
||||
|
||||
@ -267,7 +243,7 @@ namespace osu.Game
|
||||
dependencies.Cache(fileStore = new FileStore(contextFactory, Storage));
|
||||
|
||||
// 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));
|
||||
|
||||
// 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));
|
||||
|
||||
KeyBindingStore = new RealmKeyBindingStore(realmFactory);
|
||||
KeyBindingStore.Register(globalBindings);
|
||||
|
||||
foreach (var r in RulesetStore.AvailableRulesets)
|
||||
KeyBindingStore.Register(r);
|
||||
KeyBindingStore.Register(globalBindings, RulesetStore.AvailableRulesets);
|
||||
|
||||
dependencies.Cache(globalBindings);
|
||||
|
||||
@ -368,6 +341,35 @@ namespace osu.Game
|
||||
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 void updateThreadStateChanged(ValueChangedEvent<GameThreadState> state)
|
||||
|
@ -127,7 +127,7 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
Padding = new MarginPadding { Horizontal = 10 },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
generalFilter = new BeatmapSearchMultipleSelectionFilterRow<SearchGeneral>(BeatmapsStrings.ListingSearchFiltersGeneral),
|
||||
generalFilter = new BeatmapSearchGeneralFilterRow(),
|
||||
modeFilter = new BeatmapSearchRulesetFilterRow(),
|
||||
categoryFilter = new BeatmapSearchFilterRow<SearchCategory>(BeatmapsStrings.ListingSearchFiltersStatus),
|
||||
genreFilter = new BeatmapSearchFilterRow<SearchGenre>(BeatmapsStrings.ListingSearchFiltersGenre),
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -71,10 +71,10 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private Color4 getStateColour() => Active.Value ? colourProvider.Content1 : colourProvider.Light2;
|
||||
protected virtual Color4 GetStateColour() => Active.Value ? colourProvider.Content1 : colourProvider.Light2;
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,10 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
|
||||
[LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.GeneralFollows))]
|
||||
[Description("Subscribed mappers")]
|
||||
Follows
|
||||
Follows,
|
||||
|
||||
[LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.GeneralFeaturedArtists))]
|
||||
[Description("Featured artists")]
|
||||
FeaturedArtists
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,8 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osuTK;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.API;
|
||||
@ -14,6 +16,7 @@ using osu.Game.Online.API.Requests;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Select.Leaderboards;
|
||||
using osu.Game.Users;
|
||||
|
||||
@ -42,34 +45,46 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private ScoreManager scoreManager { get; set; }
|
||||
|
||||
private GetScoresRequest getScoresRequest;
|
||||
|
||||
private CancellationTokenSource loadCancellationSource;
|
||||
|
||||
protected APILegacyScores Scores
|
||||
{
|
||||
set => Schedule(() =>
|
||||
{
|
||||
loadCancellationSource?.Cancel();
|
||||
loadCancellationSource = new CancellationTokenSource();
|
||||
|
||||
topScoresContainer.Clear();
|
||||
scoreTable.ClearScores();
|
||||
scoreTable.Hide();
|
||||
|
||||
if (value?.Scores.Any() != true)
|
||||
{
|
||||
scoreTable.ClearScores();
|
||||
scoreTable.Hide();
|
||||
return;
|
||||
}
|
||||
|
||||
var scoreInfos = value.Scores.Select(s => s.CreateScoreInfo(rulesets)).ToList();
|
||||
var topScore = scoreInfos.First();
|
||||
scoreManager.OrderByTotalScoreAsync(value.Scores.Select(s => s.CreateScoreInfo(rulesets)).ToArray(), loadCancellationSource.Token)
|
||||
.ContinueWith(ordered => Schedule(() =>
|
||||
{
|
||||
if (loadCancellationSource.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
scoreTable.DisplayScores(scoreInfos, topScore.Beatmap?.Status.GrantsPerformancePoints() == true);
|
||||
scoreTable.Show();
|
||||
var topScore = ordered.Result.First();
|
||||
|
||||
var userScore = value.UserScore;
|
||||
var userScoreInfo = userScore?.Score.CreateScoreInfo(rulesets);
|
||||
scoreTable.DisplayScores(ordered.Result, topScore.Beatmap?.Status.GrantsPerformancePoints() == true);
|
||||
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(userScoreInfo, userScore.Position));
|
||||
topScoresContainer.Add(new DrawableTopScore(topScore));
|
||||
|
||||
if (userScoreInfo != null && userScoreInfo.OnlineScoreID != topScore.OnlineScoreID)
|
||||
topScoresContainer.Add(new DrawableTopScore(userScoreInfo, userScore.Position));
|
||||
}), TaskContinuationOptions.OnlyOnRanToCompletion);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -4,10 +4,13 @@
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.MatrixExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Handlers.Tablet;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osuTK;
|
||||
@ -17,6 +20,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
{
|
||||
public class TabletAreaSelection : CompositeDrawable
|
||||
{
|
||||
public bool IsWithinBounds { get; private set; }
|
||||
|
||||
private readonly ITabletHandler handler;
|
||||
|
||||
private Container tabletContainer;
|
||||
@ -109,29 +114,30 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
areaOffset.BindTo(handler.AreaOffset);
|
||||
areaOffset.BindValueChanged(val =>
|
||||
{
|
||||
usableAreaContainer.MoveTo(val.NewValue, 100, Easing.OutQuint)
|
||||
.OnComplete(_ => checkBounds()); // required as we are using SSDQ.
|
||||
usableAreaContainer.MoveTo(val.NewValue, 100, Easing.OutQuint);
|
||||
checkBounds();
|
||||
}, true);
|
||||
|
||||
areaSize.BindTo(handler.AreaSize);
|
||||
areaSize.BindValueChanged(val =>
|
||||
{
|
||||
usableAreaContainer.ResizeTo(val.NewValue, 100, Easing.OutQuint)
|
||||
.OnComplete(_ => checkBounds()); // required as we are using SSDQ.
|
||||
usableAreaContainer.ResizeTo(val.NewValue, 100, Easing.OutQuint);
|
||||
|
||||
int x = (int)val.NewValue.X;
|
||||
int y = (int)val.NewValue.Y;
|
||||
int commonDivider = greatestCommonDivider(x, y);
|
||||
|
||||
usableAreaText.Text = $"{(float)x / commonDivider}:{(float)y / commonDivider}";
|
||||
checkBounds();
|
||||
}, true);
|
||||
|
||||
rotation.BindTo(handler.Rotation);
|
||||
rotation.BindValueChanged(val =>
|
||||
{
|
||||
usableAreaContainer.RotateTo(val.NewValue, 100, 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);
|
||||
|
||||
tablet.BindTo(handler.Tablet);
|
||||
@ -169,12 +175,35 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
if (tablet.Value == null)
|
||||
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)) &&
|
||||
tabletContainer.ScreenSpaceDrawQuad.Contains(usableSsdq.BottomRight - new Vector2(1));
|
||||
var tabletArea = new Quad(-lenience, -lenience, tablet.Value.Size.X + lenience * 2, tablet.Value.Size.Y + lenience * 2);
|
||||
|
||||
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()
|
||||
|
@ -20,6 +20,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
{
|
||||
public class TabletSettings : SettingsSubsection
|
||||
{
|
||||
public TabletAreaSelection AreaSelection { get; private set; }
|
||||
|
||||
private readonly ITabletHandler tabletHandler;
|
||||
|
||||
private readonly Bindable<bool> enabled = new BindableBool(true);
|
||||
@ -121,7 +123,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new TabletAreaSelection(tabletHandler)
|
||||
AreaSelection = new TabletAreaSelection(tabletHandler)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 300,
|
||||
|
@ -13,6 +13,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Database;
|
||||
@ -36,6 +37,7 @@ namespace osu.Game.Scoring
|
||||
|
||||
private readonly RulesetStore rulesets;
|
||||
private readonly Func<BeatmapManager> beatmaps;
|
||||
private readonly Scheduler scheduler;
|
||||
|
||||
[CanBeNull]
|
||||
private readonly Func<BeatmapDifficultyCache> difficulties;
|
||||
@ -43,12 +45,13 @@ namespace osu.Game.Scoring
|
||||
[CanBeNull]
|
||||
private readonly OsuConfigManager configManager;
|
||||
|
||||
public ScoreManager(RulesetStore rulesets, Func<BeatmapManager> beatmaps, Storage storage, IAPIProvider api, IDatabaseContextFactory contextFactory, IIpcHost importHost = null,
|
||||
Func<BeatmapDifficultyCache> difficulties = null, OsuConfigManager configManager = null)
|
||||
public ScoreManager(RulesetStore rulesets, Func<BeatmapManager> beatmaps, Storage storage, IAPIProvider api, IDatabaseContextFactory contextFactory, Scheduler scheduler,
|
||||
IIpcHost importHost = null, Func<BeatmapDifficultyCache> difficulties = null, OsuConfigManager configManager = null)
|
||||
: base(storage, contextFactory, api, new ScoreStore(contextFactory, storage), importHost)
|
||||
{
|
||||
this.rulesets = rulesets;
|
||||
this.beatmaps = beatmaps;
|
||||
this.scheduler = scheduler;
|
||||
this.difficulties = difficulties;
|
||||
this.configManager = configManager;
|
||||
}
|
||||
@ -103,6 +106,32 @@ namespace osu.Game.Scoring
|
||||
=> base.CheckLocalAvailability(model, items)
|
||||
|| (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>
|
||||
/// Retrieves a bindable that represents the total score of a <see cref="ScoreInfo"/>.
|
||||
/// </summary>
|
||||
@ -111,9 +140,9 @@ namespace osu.Game.Scoring
|
||||
/// </remarks>
|
||||
/// <param name="score">The <see cref="ScoreInfo"/> to retrieve the bindable for.</param>
|
||||
/// <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);
|
||||
return bindable;
|
||||
}
|
||||
@ -126,7 +155,83 @@ namespace osu.Game.Scoring
|
||||
/// </remarks>
|
||||
/// <param name="score">The <see cref="ScoreInfo"/> to retrieve the bindable for.</param>
|
||||
/// <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>
|
||||
/// 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>();
|
||||
|
||||
private readonly ScoreInfo score;
|
||||
private readonly Func<BeatmapDifficultyCache> difficulties;
|
||||
private readonly ScoreManager scoreManager;
|
||||
|
||||
private CancellationTokenSource difficultyCalculationCancellationSource;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="TotalScoreBindable"/>.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
public TotalScoreBindable(ScoreInfo score, Func<BeatmapDifficultyCache> difficulties)
|
||||
/// <param name="scoreManager">The <see cref="ScoreManager"/>.</param>
|
||||
public TotalScoreBindable(ScoreInfo score, ScoreManager scoreManager)
|
||||
{
|
||||
this.score = score;
|
||||
this.difficulties = difficulties;
|
||||
this.scoreManager = scoreManager;
|
||||
|
||||
ScoringMode.BindValueChanged(onScoringModeChanged, true);
|
||||
}
|
||||
|
||||
private IBindable<StarDifficulty?> difficultyBindable;
|
||||
private CancellationTokenSource difficultyCancellationSource;
|
||||
|
||||
private void onScoringModeChanged(ValueChangedEvent<ScoringMode> mode)
|
||||
{
|
||||
difficultyCancellationSource?.Cancel();
|
||||
difficultyCancellationSource = null;
|
||||
difficultyCalculationCancellationSource?.Cancel();
|
||||
difficultyCalculationCancellationSource = new CancellationTokenSource();
|
||||
|
||||
if (score.Beatmap == null)
|
||||
{
|
||||
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));
|
||||
scoreManager.GetTotalScore(score, s => Value = s, mode.NewValue, difficultyCalculationCancellationSource.Token);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -17,15 +17,21 @@ namespace osu.Game.Screens
|
||||
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)
|
||||
return;
|
||||
return false;
|
||||
|
||||
if (EqualityComparer<BackgroundScreen>.Default.Equals((BackgroundScreen)CurrentScreen, screen))
|
||||
return;
|
||||
return false;
|
||||
|
||||
base.Push(screen);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
27
osu.Game/Screens/Edit/Components/Menus/DifficultyMenuItem.cs
Normal file
27
osu.Game/Screens/Edit/Components/Menus/DifficultyMenuItem.cs
Normal 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;
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
@ -75,6 +76,9 @@ namespace osu.Game.Screens.Edit
|
||||
|
||||
private Container<EditorScreen> screenContainer;
|
||||
|
||||
[CanBeNull]
|
||||
private readonly EditorLoader loader;
|
||||
|
||||
private EditorScreen currentScreen;
|
||||
|
||||
private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor();
|
||||
@ -101,6 +105,11 @@ namespace osu.Game.Screens.Edit
|
||||
[Resolved]
|
||||
private MusicController music { get; set; }
|
||||
|
||||
public Editor(EditorLoader loader = null)
|
||||
{
|
||||
this.loader = loader;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours, OsuConfigManager config)
|
||||
{
|
||||
@ -489,7 +498,7 @@ namespace osu.Game.Screens.Edit
|
||||
|
||||
if (isNewBeatmap || HasUnsavedChanges)
|
||||
{
|
||||
dialogOverlay?.Push(new PromptForSaveDialog(confirmExit, confirmExitWithSave));
|
||||
dialogOverlay?.Push(new PromptForSaveDialog(confirmExit, confirmExitWithSave, cancelExit));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -703,11 +712,38 @@ namespace osu.Game.Screens.Edit
|
||||
if (RuntimeInfo.IsDesktop)
|
||||
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 EditorMenuItem("Exit", MenuItemType.Standard, this.Exit));
|
||||
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 GetBeatLengthAtTime(double referenceTime) => editorBeatmap.GetBeatLengthAtTime(referenceTime);
|
||||
|
94
osu.Game/Screens/Edit/EditorLoader.cs
Normal file
94
osu.Game/Screens/Edit/EditorLoader.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -9,7 +9,7 @@ namespace osu.Game.Screens.Edit
|
||||
{
|
||||
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?";
|
||||
|
||||
@ -30,6 +30,7 @@ namespace osu.Game.Screens.Edit
|
||||
new PopupDialogCancelButton
|
||||
{
|
||||
Text = @"Oops, continue editing",
|
||||
Action = cancel
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -103,7 +103,7 @@ namespace osu.Game.Screens.Menu
|
||||
OnEdit = delegate
|
||||
{
|
||||
Beatmap.SetDefault();
|
||||
this.Push(new Editor());
|
||||
this.Push(new EditorLoader());
|
||||
},
|
||||
OnSolo = loadSoloSongSelect,
|
||||
OnMultiplayer = () => this.Push(new Multiplayer()),
|
||||
|
@ -32,6 +32,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private ScoreManager scoreManager { get; set; }
|
||||
|
||||
public PlaylistsResultsScreen(ScoreInfo score, long roomId, PlaylistItem playlistItem, bool allowRetry, bool allowWatchingReplay = true)
|
||||
: 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>
|
||||
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.
|
||||
// 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)
|
||||
// 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,
|
||||
// calculate the total scores locally before invoking the success callback.
|
||||
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.
|
||||
SelectedScore.Value = scoreInfos.FirstOrDefault(s => s.User.Id == api.LocalUser.Value.Id) ?? scoreInfos.FirstOrDefault();
|
||||
});
|
||||
}
|
||||
Schedule(() =>
|
||||
{
|
||||
// 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.
|
||||
callback.Invoke(scoreInfos.Where(s => s.OnlineScoreID != Score?.OnlineScoreID));
|
||||
// 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));
|
||||
|
||||
hideLoadingSpinners(pivot);
|
||||
hideLoadingSpinners(pivot);
|
||||
}));
|
||||
}
|
||||
|
||||
private void hideLoadingSpinners([CanBeNull] MultiplayerScores pivot = null)
|
||||
|
@ -186,17 +186,14 @@ namespace osu.Game.Screens
|
||||
{
|
||||
applyArrivingDefaults(false);
|
||||
|
||||
backgroundStack?.Push(ownedBackground = CreateBackground());
|
||||
|
||||
background = backgroundStack?.CurrentScreen as BackgroundScreen;
|
||||
|
||||
if (background != ownedBackground)
|
||||
if (backgroundStack?.Push(ownedBackground = CreateBackground()) != true)
|
||||
{
|
||||
// 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 = null;
|
||||
}
|
||||
|
||||
background = backgroundStack?.CurrentScreen as BackgroundScreen;
|
||||
base.OnEntering(last);
|
||||
}
|
||||
|
||||
|
@ -52,8 +52,7 @@ namespace osu.Game.Screens.Ranking
|
||||
private Drawable bottomPanel;
|
||||
private Container<ScorePanel> detachedPanelContainer;
|
||||
|
||||
private bool fetchedInitialScores;
|
||||
private APIRequest nextPageRequest;
|
||||
private bool lastFetchCompleted;
|
||||
|
||||
private readonly bool allowRetry;
|
||||
private readonly bool allowWatchingReplay;
|
||||
@ -191,8 +190,10 @@ namespace osu.Game.Screens.Ranking
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (fetchedInitialScores && nextPageRequest == null)
|
||||
if (lastFetchCompleted)
|
||||
{
|
||||
APIRequest nextPageRequest = null;
|
||||
|
||||
if (ScorePanelList.IsScrolledToStart)
|
||||
nextPageRequest = FetchNextPage(-1, fetchScoresCallback);
|
||||
else if (ScorePanelList.IsScrolledToEnd)
|
||||
@ -200,10 +201,7 @@ namespace osu.Game.Screens.Ranking
|
||||
|
||||
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.
|
||||
nextPageRequest.Success += () => ScheduleAfterChildren(() => nextPageRequest = null);
|
||||
nextPageRequest.Failure += _ => ScheduleAfterChildren(() => nextPageRequest = null);
|
||||
|
||||
lastFetchCompleted = false;
|
||||
api.Queue(nextPageRequest);
|
||||
}
|
||||
}
|
||||
@ -229,7 +227,7 @@ namespace osu.Game.Screens.Ranking
|
||||
foreach (var s in scores)
|
||||
addScore(s);
|
||||
|
||||
fetchedInitialScores = true;
|
||||
lastFetchCompleted = true;
|
||||
});
|
||||
|
||||
public override void OnEntering(IScreen last)
|
||||
|
@ -5,10 +5,14 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Scoring;
|
||||
using osuTK;
|
||||
@ -36,12 +40,14 @@ namespace osu.Game.Screens.Ranking
|
||||
/// <summary>
|
||||
/// Whether this <see cref="ScorePanelList"/> can be scrolled and is currently scrolled to the start.
|
||||
/// </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>
|
||||
/// Whether this <see cref="ScorePanelList"/> can be scrolled and is currently scrolled to the end.
|
||||
/// </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>
|
||||
/// The current scroll position.
|
||||
@ -60,6 +66,13 @@ namespace osu.Game.Screens.Ranking
|
||||
|
||||
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 Scroll scroll;
|
||||
private ScorePanel expandedPanel;
|
||||
@ -90,6 +103,9 @@ namespace osu.Game.Screens.Ranking
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
foreach (var d in flow)
|
||||
displayScore(d);
|
||||
|
||||
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.Origin = Anchor.Centre;
|
||||
}));
|
||||
d.Hide();
|
||||
});
|
||||
|
||||
flow.Add(trackingContainer);
|
||||
|
||||
if (IsLoaded)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
displayScore(trackingContainer);
|
||||
|
||||
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>
|
||||
/// Brings a <see cref="ScoreInfo"/> to the centre of the screen and expands it.
|
||||
/// </summary>
|
||||
@ -267,6 +303,9 @@ namespace osu.Game.Screens.Ranking
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
if (expandedPanel == null)
|
||||
return base.OnKeyDown(e);
|
||||
|
||||
var expandedPanelIndex = flow.GetPanelIndex(expandedPanel.Score);
|
||||
|
||||
switch (e.Key)
|
||||
@ -285,6 +324,12 @@ namespace osu.Game.Screens.Ranking
|
||||
return base.OnKeyDown(e);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
loadCancellationSource?.Cancel();
|
||||
}
|
||||
|
||||
private class Flow : FillFlowContainer<ScorePanelTrackingContainer>
|
||||
{
|
||||
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();
|
||||
|
||||
private IEnumerable<ScorePanelTrackingContainer> applySorting(IEnumerable<Drawable> drawables) => drawables.OfType<ScorePanelTrackingContainer>()
|
||||
.OrderByDescending(s => s.Panel.Score.TotalScore)
|
||||
.OrderByDescending(GetLayoutPosition)
|
||||
.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
|
||||
|
@ -4,6 +4,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Beatmaps;
|
||||
@ -66,6 +68,9 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
[Resolved]
|
||||
private ScoreManager scoreManager { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private BeatmapDifficultyCache difficultyCache { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private IBindable<RulesetInfo> ruleset { get; set; }
|
||||
|
||||
@ -120,8 +125,13 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
|
||||
protected override bool IsOnlineScope => Scope != BeatmapLeaderboardScope.Local;
|
||||
|
||||
private CancellationTokenSource loadCancellationSource;
|
||||
|
||||
protected override APIRequest FetchScores(Action<IEnumerable<ScoreInfo>> scoresCallback)
|
||||
{
|
||||
loadCancellationSource?.Cancel();
|
||||
loadCancellationSource = new CancellationTokenSource();
|
||||
|
||||
if (Beatmap == null)
|
||||
{
|
||||
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.OrderByDescending(s => s.TotalScore).ToArray();
|
||||
PlaceholderState = Scores.Any() ? PlaceholderState.Successful : PlaceholderState.NoScores;
|
||||
scoreManager.OrderByTotalScoreAsync(scores.ToArray(), loadCancellationSource.Token)
|
||||
.ContinueWith(ordered => scoresCallback?.Invoke(ordered.Result), TaskContinuationOptions.OnlyOnRanToCompletion);
|
||||
|
||||
return null;
|
||||
}
|
||||
@ -182,8 +192,15 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
|
||||
req.Success += r =>
|
||||
{
|
||||
scoresCallback?.Invoke(r.Scores.Select(s => s.CreateScoreInfo(rulesets)));
|
||||
TopScore = r.UserScore?.CreateScoreInfo(rulesets);
|
||||
scoreManager.OrderByTotalScoreAsync(r.Scores.Select(s => s.CreateScoreInfo(rulesets)).ToArray(), loadCancellationSource.Token)
|
||||
.ContinueWith(ordered => Schedule(() =>
|
||||
{
|
||||
if (loadCancellationSource.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
scoresCallback?.Invoke(ordered.Result);
|
||||
TopScore = r.UserScore?.CreateScoreInfo(rulesets);
|
||||
}), TaskContinuationOptions.OnlyOnRanToCompletion);
|
||||
};
|
||||
|
||||
return req;
|
||||
|
@ -349,7 +349,7 @@ namespace osu.Game.Screens.Select
|
||||
throw new InvalidOperationException($"Attempted to edit when {nameof(AllowEditing)} is disabled");
|
||||
|
||||
Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap ?? beatmapNoDebounce);
|
||||
this.Push(new Editor());
|
||||
this.Push(new EditorLoader());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -367,6 +367,11 @@ namespace osu.Game.Tests.Visual
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -36,8 +36,8 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Realm" Version="10.3.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2021.830.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.827.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2021.907.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.907.0" />
|
||||
<PackageReference Include="Sentry" Version="3.9.0" />
|
||||
<PackageReference Include="SharpCompress" Version="0.28.3" />
|
||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||
|
@ -70,8 +70,8 @@
|
||||
<Reference Include="System.Net.Http" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.830.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.827.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.907.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.907.0" />
|
||||
</ItemGroup>
|
||||
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
|
||||
<PropertyGroup>
|
||||
@ -93,7 +93,7 @@
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="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="NUnit" Version="3.13.2" />
|
||||
<PackageReference Include="SharpRaven" Version="2.4.0" />
|
||||
|
Loading…
x
Reference in New Issue
Block a user