mirror of
https://github.com/ppy/osu.git
synced 2025-03-28 01:37:46 +08:00
Merge branch 'master' into chat-command
This commit is contained in:
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" />
|
<Reference Include="Java.Interop" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.827.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.907.0" />
|
||||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.830.0" />
|
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.907.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Transitive Dependencies">
|
<ItemGroup Label="Transitive Dependencies">
|
||||||
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
|
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
|
||||||
|
@ -11,6 +11,7 @@ using osu.Framework.Platform;
|
|||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
using osu.Game.Input;
|
using osu.Game.Input;
|
||||||
using osu.Game.Input.Bindings;
|
using osu.Game.Input.Bindings;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
using Realms;
|
using Realms;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Database
|
namespace osu.Game.Tests.Database
|
||||||
@ -42,7 +43,7 @@ namespace osu.Game.Tests.Database
|
|||||||
|
|
||||||
KeyBindingContainer testContainer = new TestKeyBindingContainer();
|
KeyBindingContainer testContainer = new TestKeyBindingContainer();
|
||||||
|
|
||||||
keyBindingStore.Register(testContainer);
|
keyBindingStore.Register(testContainer, Enumerable.Empty<RulesetInfo>());
|
||||||
|
|
||||||
Assert.That(queryCount(), Is.EqualTo(3));
|
Assert.That(queryCount(), Is.EqualTo(3));
|
||||||
|
|
||||||
@ -66,7 +67,7 @@ namespace osu.Game.Tests.Database
|
|||||||
{
|
{
|
||||||
KeyBindingContainer testContainer = new TestKeyBindingContainer();
|
KeyBindingContainer testContainer = new TestKeyBindingContainer();
|
||||||
|
|
||||||
keyBindingStore.Register(testContainer);
|
keyBindingStore.Register(testContainer, Enumerable.Empty<RulesetInfo>());
|
||||||
|
|
||||||
using (var primaryUsage = realmContextFactory.GetForRead())
|
using (var primaryUsage = realmContextFactory.GetForRead())
|
||||||
{
|
{
|
||||||
|
@ -40,10 +40,10 @@ namespace osu.Game.Tests.NonVisual.Skinning
|
|||||||
assertPlaybackPosition(0);
|
assertPlaybackPosition(0);
|
||||||
|
|
||||||
AddStep("set start time to 1000", () => animationTimeReference.AnimationStartTime.Value = 1000);
|
AddStep("set start time to 1000", () => animationTimeReference.AnimationStartTime.Value = 1000);
|
||||||
assertPlaybackPosition(-1000);
|
assertPlaybackPosition(0);
|
||||||
|
|
||||||
AddStep("set current time to 500", () => animationTimeReference.ManualClock.CurrentTime = 500);
|
AddStep("set current time to 500", () => animationTimeReference.ManualClock.CurrentTime = 500);
|
||||||
assertPlaybackPosition(-500);
|
assertPlaybackPosition(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertPlaybackPosition(double expectedPosition)
|
private void assertPlaybackPosition(double expectedPosition)
|
||||||
|
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 }
|
Ruleset = { Value = new OsuRuleset().RulesetInfo }
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
AddUntilStep("wait for load", () => resultsScreen.ChildrenOfType<ScorePanelList>().FirstOrDefault()?.AllPanelsVisible == true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void waitForDisplay()
|
private void waitForDisplay()
|
||||||
{
|
{
|
||||||
AddUntilStep("wait for request to complete", () => requestComplete);
|
AddUntilStep("wait for request to complete", () => requestComplete);
|
||||||
|
AddUntilStep("wait for panels to be visible", () => resultsScreen.ChildrenOfType<ScorePanelList>().FirstOrDefault()?.AllPanelsVisible == true);
|
||||||
AddWaitStep("wait for display", 5);
|
AddWaitStep("wait for display", 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,7 +99,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
|||||||
TestResultsScreen screen = null;
|
TestResultsScreen screen = null;
|
||||||
|
|
||||||
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
|
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
|
||||||
AddUntilStep("wait for loaded", () => screen.IsLoaded);
|
AddUntilStep("wait for load", () => this.ChildrenOfType<ScorePanelList>().Single().AllPanelsVisible);
|
||||||
|
|
||||||
AddStep("click expanded panel", () =>
|
AddStep("click expanded panel", () =>
|
||||||
{
|
{
|
||||||
@ -138,7 +138,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
|||||||
TestResultsScreen screen = null;
|
TestResultsScreen screen = null;
|
||||||
|
|
||||||
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
|
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
|
||||||
AddUntilStep("wait for loaded", () => screen.IsLoaded);
|
AddUntilStep("wait for load", () => this.ChildrenOfType<ScorePanelList>().Single().AllPanelsVisible);
|
||||||
|
|
||||||
AddStep("click expanded panel", () =>
|
AddStep("click expanded panel", () =>
|
||||||
{
|
{
|
||||||
@ -177,7 +177,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
|||||||
TestResultsScreen screen = null;
|
TestResultsScreen screen = null;
|
||||||
|
|
||||||
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
|
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
|
||||||
AddUntilStep("wait for loaded", () => screen.IsLoaded);
|
AddUntilStep("wait for load", () => this.ChildrenOfType<ScorePanelList>().Single().AllPanelsVisible);
|
||||||
|
|
||||||
ScorePanel expandedPanel = null;
|
ScorePanel expandedPanel = null;
|
||||||
ScorePanel contractedPanel = null;
|
ScorePanel contractedPanel = null;
|
||||||
@ -223,6 +223,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
|||||||
TestResultsScreen screen = null;
|
TestResultsScreen screen = null;
|
||||||
|
|
||||||
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
|
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
|
||||||
|
AddUntilStep("wait for load", () => this.ChildrenOfType<ScorePanelList>().Single().AllPanelsVisible);
|
||||||
|
|
||||||
AddAssert("download button is disabled", () => !screen.ChildrenOfType<DownloadButton>().Last().Enabled.Value);
|
AddAssert("download button is disabled", () => !screen.ChildrenOfType<DownloadButton>().Last().Enabled.Value);
|
||||||
|
|
||||||
|
@ -159,6 +159,9 @@ namespace osu.Game.Tests.Visual.Ranking
|
|||||||
var firstScore = new TestScoreInfo(new OsuRuleset().RulesetInfo);
|
var firstScore = new TestScoreInfo(new OsuRuleset().RulesetInfo);
|
||||||
var secondScore = new TestScoreInfo(new OsuRuleset().RulesetInfo);
|
var secondScore = new TestScoreInfo(new OsuRuleset().RulesetInfo);
|
||||||
|
|
||||||
|
firstScore.User.Username = "A";
|
||||||
|
secondScore.User.Username = "B";
|
||||||
|
|
||||||
createListStep(() => new ScorePanelList());
|
createListStep(() => new ScorePanelList());
|
||||||
|
|
||||||
AddStep("add scores and select first", () =>
|
AddStep("add scores and select first", () =>
|
||||||
@ -168,6 +171,8 @@ namespace osu.Game.Tests.Visual.Ranking
|
|||||||
list.SelectedScore.Value = firstScore;
|
list.SelectedScore.Value = firstScore;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
AddUntilStep("wait for load", () => list.AllPanelsVisible);
|
||||||
|
|
||||||
assertScoreState(firstScore, true);
|
assertScoreState(firstScore, true);
|
||||||
assertScoreState(secondScore, false);
|
assertScoreState(secondScore, false);
|
||||||
|
|
||||||
@ -182,6 +187,22 @@ namespace osu.Game.Tests.Visual.Ranking
|
|||||||
assertExpandedPanelCentred();
|
assertExpandedPanelCentred();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestAddScoreImmediately()
|
||||||
|
{
|
||||||
|
var score = new TestScoreInfo(new OsuRuleset().RulesetInfo);
|
||||||
|
|
||||||
|
createListStep(() =>
|
||||||
|
{
|
||||||
|
var newList = new ScorePanelList { SelectedScore = { Value = score } };
|
||||||
|
newList.AddScore(score);
|
||||||
|
return newList;
|
||||||
|
});
|
||||||
|
|
||||||
|
assertScoreState(score, true);
|
||||||
|
assertExpandedPanelCentred();
|
||||||
|
}
|
||||||
|
|
||||||
private void createListStep(Func<ScorePanelList> creationFunc)
|
private void createListStep(Func<ScorePanelList> creationFunc)
|
||||||
{
|
{
|
||||||
AddStep("create list", () => Child = list = creationFunc().With(d =>
|
AddStep("create list", () => Child = list = creationFunc().With(d =>
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Input.Handlers.Tablet;
|
using osu.Framework.Input.Handlers.Tablet;
|
||||||
using osu.Framework.Platform;
|
using osu.Framework.Testing;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
|
using osu.Game.Overlays.Settings;
|
||||||
using osu.Game.Overlays.Settings.Sections.Input;
|
using osu.Game.Overlays.Settings.Sections.Input;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
@ -17,22 +18,34 @@ namespace osu.Game.Tests.Visual.Settings
|
|||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class TestSceneTabletSettings : OsuTestScene
|
public class TestSceneTabletSettings : OsuTestScene
|
||||||
{
|
{
|
||||||
[BackgroundDependencyLoader]
|
private TestTabletHandler tabletHandler;
|
||||||
private void load(GameHost host)
|
private TabletSettings settings;
|
||||||
{
|
|
||||||
var tabletHandler = new TestTabletHandler();
|
|
||||||
|
|
||||||
AddRange(new Drawable[]
|
[SetUpSteps]
|
||||||
|
public void SetUpSteps()
|
||||||
|
{
|
||||||
|
AddStep("create settings", () =>
|
||||||
{
|
{
|
||||||
new TabletSettings(tabletHandler)
|
tabletHandler = new TestTabletHandler();
|
||||||
|
|
||||||
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.None,
|
settings = new TabletSettings(tabletHandler)
|
||||||
Width = SettingsPanel.PANEL_WIDTH,
|
{
|
||||||
Anchor = Anchor.TopCentre,
|
RelativeSizeAxes = Axes.None,
|
||||||
Origin = Anchor.TopCentre,
|
Width = SettingsPanel.PANEL_WIDTH,
|
||||||
}
|
Anchor = Anchor.TopCentre,
|
||||||
|
Origin = Anchor.TopCentre,
|
||||||
|
}
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
AddStep("set square size", () => tabletHandler.SetTabletSize(new Vector2(100, 100)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestVariousTabletSizes()
|
||||||
|
{
|
||||||
AddStep("Test with wide tablet", () => tabletHandler.SetTabletSize(new Vector2(160, 100)));
|
AddStep("Test with wide tablet", () => tabletHandler.SetTabletSize(new Vector2(160, 100)));
|
||||||
AddStep("Test with square tablet", () => tabletHandler.SetTabletSize(new Vector2(300, 300)));
|
AddStep("Test with square tablet", () => tabletHandler.SetTabletSize(new Vector2(300, 300)));
|
||||||
AddStep("Test with tall tablet", () => tabletHandler.SetTabletSize(new Vector2(100, 300)));
|
AddStep("Test with tall tablet", () => tabletHandler.SetTabletSize(new Vector2(100, 300)));
|
||||||
@ -40,6 +53,71 @@ namespace osu.Game.Tests.Visual.Settings
|
|||||||
AddStep("Test no tablet present", () => tabletHandler.SetTabletSize(Vector2.Zero));
|
AddStep("Test no tablet present", () => tabletHandler.SetTabletSize(Vector2.Zero));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestWideAspectRatioValidity()
|
||||||
|
{
|
||||||
|
AddStep("Test with wide tablet", () => tabletHandler.SetTabletSize(new Vector2(160, 100)));
|
||||||
|
|
||||||
|
AddStep("Reset to full area", () => settings.ChildrenOfType<DangerousSettingsButton>().First().TriggerClick());
|
||||||
|
ensureValid();
|
||||||
|
|
||||||
|
AddStep("rotate 10", () => tabletHandler.Rotation.Value = 10);
|
||||||
|
ensureInvalid();
|
||||||
|
|
||||||
|
AddStep("scale down", () => tabletHandler.AreaSize.Value *= 0.9f);
|
||||||
|
ensureInvalid();
|
||||||
|
|
||||||
|
AddStep("scale down", () => tabletHandler.AreaSize.Value *= 0.9f);
|
||||||
|
ensureInvalid();
|
||||||
|
|
||||||
|
AddStep("scale down", () => tabletHandler.AreaSize.Value *= 0.9f);
|
||||||
|
ensureValid();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestRotationValidity()
|
||||||
|
{
|
||||||
|
AddAssert("area valid", () => settings.AreaSelection.IsWithinBounds);
|
||||||
|
|
||||||
|
AddStep("rotate 90", () => tabletHandler.Rotation.Value = 90);
|
||||||
|
ensureValid();
|
||||||
|
|
||||||
|
AddStep("rotate 180", () => tabletHandler.Rotation.Value = 180);
|
||||||
|
|
||||||
|
ensureValid();
|
||||||
|
|
||||||
|
AddStep("rotate 270", () => tabletHandler.Rotation.Value = 270);
|
||||||
|
|
||||||
|
ensureValid();
|
||||||
|
|
||||||
|
AddStep("rotate 360", () => tabletHandler.Rotation.Value = 360);
|
||||||
|
|
||||||
|
ensureValid();
|
||||||
|
|
||||||
|
AddStep("rotate 0", () => tabletHandler.Rotation.Value = 0);
|
||||||
|
ensureValid();
|
||||||
|
|
||||||
|
AddStep("rotate 45", () => tabletHandler.Rotation.Value = 45);
|
||||||
|
ensureInvalid();
|
||||||
|
|
||||||
|
AddStep("rotate 0", () => tabletHandler.Rotation.Value = 0);
|
||||||
|
ensureValid();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestOffsetValidity()
|
||||||
|
{
|
||||||
|
ensureValid();
|
||||||
|
AddStep("move right", () => tabletHandler.AreaOffset.Value = Vector2.Zero);
|
||||||
|
ensureInvalid();
|
||||||
|
AddStep("move back", () => tabletHandler.AreaOffset.Value = tabletHandler.AreaSize.Value / 2);
|
||||||
|
ensureValid();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureValid() => AddAssert("area valid", () => settings.AreaSelection.IsWithinBounds);
|
||||||
|
|
||||||
|
private void ensureInvalid() => AddAssert("area invalid", () => !settings.AreaSelection.IsWithinBounds);
|
||||||
|
|
||||||
public class TestTabletHandler : ITabletHandler
|
public class TestTabletHandler : ITabletHandler
|
||||||
{
|
{
|
||||||
public Bindable<Vector2> AreaOffset { get; } = new Bindable<Vector2>();
|
public Bindable<Vector2> AreaOffset { get; } = new Bindable<Vector2>();
|
||||||
|
@ -41,7 +41,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
|
|
||||||
dependencies.Cache(rulesetStore = new RulesetStore(ContextFactory));
|
dependencies.Cache(rulesetStore = new RulesetStore(ContextFactory));
|
||||||
dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesetStore, null, dependencies.Get<AudioManager>(), Resources, dependencies.Get<GameHost>(), Beatmap.Default));
|
dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesetStore, null, dependencies.Get<AudioManager>(), Resources, dependencies.Get<GameHost>(), Beatmap.Default));
|
||||||
dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, null, ContextFactory));
|
dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, null, ContextFactory, Scheduler));
|
||||||
|
|
||||||
return dependencies;
|
return dependencies;
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Humanizer;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
@ -73,7 +74,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
};
|
};
|
||||||
|
|
||||||
control.Query.BindValueChanged(q => query.Text = $"Query: {q.NewValue}", true);
|
control.Query.BindValueChanged(q => query.Text = $"Query: {q.NewValue}", true);
|
||||||
control.General.BindCollectionChanged((u, v) => general.Text = $"General: {(control.General.Any() ? string.Join('.', control.General.Select(i => i.ToString().ToLowerInvariant())) : "")}", true);
|
control.General.BindCollectionChanged((u, v) => general.Text = $"General: {(control.General.Any() ? string.Join('.', control.General.Select(i => i.ToString().Underscore())) : "")}", true);
|
||||||
control.Ruleset.BindValueChanged(r => ruleset.Text = $"Ruleset: {r.NewValue}", true);
|
control.Ruleset.BindValueChanged(r => ruleset.Text = $"Ruleset: {r.NewValue}", true);
|
||||||
control.Category.BindValueChanged(c => category.Text = $"Category: {c.NewValue}", true);
|
control.Category.BindValueChanged(c => category.Text = $"Category: {c.NewValue}", true);
|
||||||
control.Genre.BindValueChanged(g => genre.Text = $"Genre: {g.NewValue}", true);
|
control.Genre.BindValueChanged(g => genre.Text = $"Genre: {g.NewValue}", true);
|
||||||
|
@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
private BeatmapManager beatmapManager;
|
private BeatmapManager beatmapManager;
|
||||||
private ScoreManager scoreManager;
|
private ScoreManager scoreManager;
|
||||||
|
|
||||||
private readonly List<ScoreInfo> scores = new List<ScoreInfo>();
|
private readonly List<ScoreInfo> importedScores = new List<ScoreInfo>();
|
||||||
private BeatmapInfo beatmap;
|
private BeatmapInfo beatmap;
|
||||||
|
|
||||||
[Cached]
|
[Cached]
|
||||||
@ -82,7 +82,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
|
|
||||||
dependencies.Cache(rulesetStore = new RulesetStore(ContextFactory));
|
dependencies.Cache(rulesetStore = new RulesetStore(ContextFactory));
|
||||||
dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesetStore, null, dependencies.Get<AudioManager>(), Resources, dependencies.Get<GameHost>(), Beatmap.Default));
|
dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesetStore, null, dependencies.Get<AudioManager>(), Resources, dependencies.Get<GameHost>(), Beatmap.Default));
|
||||||
dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, null, ContextFactory));
|
dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, null, ContextFactory, Scheduler));
|
||||||
|
|
||||||
beatmap = beatmapManager.Import(new ImportTask(TestResources.GetQuickTestBeatmapForImport())).Result.Beatmaps[0];
|
beatmap = beatmapManager.Import(new ImportTask(TestResources.GetQuickTestBeatmapForImport())).Result.Beatmaps[0];
|
||||||
|
|
||||||
@ -100,11 +100,9 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
User = new User { Username = "TestUser" },
|
User = new User { Username = "TestUser" },
|
||||||
};
|
};
|
||||||
|
|
||||||
scores.Add(scoreManager.Import(score).Result);
|
importedScores.Add(scoreManager.Import(score).Result);
|
||||||
}
|
}
|
||||||
|
|
||||||
scores.Sort(Comparer<ScoreInfo>.Create((s1, s2) => s2.TotalScore.CompareTo(s1.TotalScore)));
|
|
||||||
|
|
||||||
return dependencies;
|
return dependencies;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,9 +132,14 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestDeleteViaRightClick()
|
public void TestDeleteViaRightClick()
|
||||||
{
|
{
|
||||||
|
ScoreInfo scoreBeingDeleted = null;
|
||||||
AddStep("open menu for top score", () =>
|
AddStep("open menu for top score", () =>
|
||||||
{
|
{
|
||||||
InputManager.MoveMouseTo(leaderboard.ChildrenOfType<LeaderboardScore>().First());
|
var leaderboardScore = leaderboard.ChildrenOfType<LeaderboardScore>().First();
|
||||||
|
|
||||||
|
scoreBeingDeleted = leaderboardScore.Score;
|
||||||
|
|
||||||
|
InputManager.MoveMouseTo(leaderboardScore);
|
||||||
InputManager.Click(MouseButton.Right);
|
InputManager.Click(MouseButton.Right);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -158,14 +161,14 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
InputManager.Click(MouseButton.Left);
|
InputManager.Click(MouseButton.Left);
|
||||||
});
|
});
|
||||||
|
|
||||||
AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineScoreID != scores[0].OnlineScoreID));
|
AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineScoreID != scoreBeingDeleted.OnlineScoreID));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestDeleteViaDatabase()
|
public void TestDeleteViaDatabase()
|
||||||
{
|
{
|
||||||
AddStep("delete top score", () => scoreManager.Delete(scores[0]));
|
AddStep("delete top score", () => scoreManager.Delete(importedScores[0]));
|
||||||
AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineScoreID != scores[0].OnlineScoreID));
|
AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineScoreID != importedScores[0].OnlineScoreID));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,52 +46,53 @@ namespace osu.Game.Input
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Register a new type of <see cref="KeyBindingContainer{T}"/>, adding default bindings from <see cref="KeyBindingContainer.DefaultKeyBindings"/>.
|
/// Register all defaults for this store.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="container">The container to populate defaults from.</param>
|
/// <param name="container">The container to populate defaults from.</param>
|
||||||
public void Register(KeyBindingContainer container) => insertDefaults(container.DefaultKeyBindings);
|
/// <param name="rulesets">The rulesets to populate defaults from.</param>
|
||||||
|
public void Register(KeyBindingContainer container, IEnumerable<RulesetInfo> rulesets)
|
||||||
/// <summary>
|
|
||||||
/// Register a ruleset, adding default bindings for each of its variants.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="ruleset">The ruleset to populate defaults from.</param>
|
|
||||||
public void Register(RulesetInfo ruleset)
|
|
||||||
{
|
|
||||||
var instance = ruleset.CreateInstance();
|
|
||||||
|
|
||||||
foreach (var variant in instance.AvailableVariants)
|
|
||||||
insertDefaults(instance.GetDefaultKeyBindings(variant), ruleset.ID, variant);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void insertDefaults(IEnumerable<IKeyBinding> defaults, int? rulesetId = null, int? variant = null)
|
|
||||||
{
|
{
|
||||||
using (var usage = realmFactory.GetForWrite())
|
using (var usage = realmFactory.GetForWrite())
|
||||||
{
|
{
|
||||||
// compare counts in database vs defaults
|
// intentionally flattened to a list rather than querying against the IQueryable, as nullable fields being queried against aren't indexed.
|
||||||
foreach (var defaultsForAction in defaults.GroupBy(k => k.Action))
|
// this is much faster as a result.
|
||||||
|
var existingBindings = usage.Realm.All<RealmKeyBinding>().ToList();
|
||||||
|
|
||||||
|
insertDefaults(usage, existingBindings, container.DefaultKeyBindings);
|
||||||
|
|
||||||
|
foreach (var ruleset in rulesets)
|
||||||
{
|
{
|
||||||
int existingCount = usage.Realm.All<RealmKeyBinding>().Count(k => k.RulesetID == rulesetId && k.Variant == variant && k.ActionInt == (int)defaultsForAction.Key);
|
var instance = ruleset.CreateInstance();
|
||||||
|
foreach (var variant in instance.AvailableVariants)
|
||||||
if (defaultsForAction.Count() <= existingCount)
|
insertDefaults(usage, existingBindings, instance.GetDefaultKeyBindings(variant), ruleset.ID, variant);
|
||||||
continue;
|
|
||||||
|
|
||||||
foreach (var k in defaultsForAction.Skip(existingCount))
|
|
||||||
{
|
|
||||||
// insert any defaults which are missing.
|
|
||||||
usage.Realm.Add(new RealmKeyBinding
|
|
||||||
{
|
|
||||||
KeyCombinationString = k.KeyCombination.ToString(),
|
|
||||||
ActionInt = (int)k.Action,
|
|
||||||
RulesetID = rulesetId,
|
|
||||||
Variant = variant
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
usage.Commit();
|
usage.Commit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void insertDefaults(RealmContextFactory.RealmUsage usage, List<RealmKeyBinding> existingBindings, IEnumerable<IKeyBinding> defaults, int? rulesetId = null, int? variant = null)
|
||||||
|
{
|
||||||
|
// compare counts in database vs defaults for each action type.
|
||||||
|
foreach (var defaultsForAction in defaults.GroupBy(k => k.Action))
|
||||||
|
{
|
||||||
|
// avoid performing redundant queries when the database is empty and needs to be re-filled.
|
||||||
|
int existingCount = existingBindings.Count(k => k.RulesetID == rulesetId && k.Variant == variant && k.ActionInt == (int)defaultsForAction.Key);
|
||||||
|
|
||||||
|
if (defaultsForAction.Count() <= existingCount)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// insert any defaults which are missing.
|
||||||
|
usage.Realm.Add(defaultsForAction.Skip(existingCount).Select(k => new RealmKeyBinding
|
||||||
|
{
|
||||||
|
KeyCombinationString = k.KeyCombination.ToString(),
|
||||||
|
ActionInt = (int)k.Action,
|
||||||
|
RulesetID = rulesetId,
|
||||||
|
Variant = variant
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Keys which should not be allowed for gameplay input purposes.
|
/// Keys which should not be allowed for gameplay input purposes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -8,8 +8,9 @@ namespace osu.Game.Online.API.Requests
|
|||||||
{
|
{
|
||||||
public class GetUserRequest : APIRequest<User>
|
public class GetUserRequest : APIRequest<User>
|
||||||
{
|
{
|
||||||
private readonly string userIdentifier;
|
private readonly string lookup;
|
||||||
public readonly RulesetInfo Ruleset;
|
public readonly RulesetInfo Ruleset;
|
||||||
|
private readonly LookupType lookupType;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the currently logged-in user.
|
/// Gets the currently logged-in user.
|
||||||
@ -25,7 +26,8 @@ namespace osu.Game.Online.API.Requests
|
|||||||
/// <param name="ruleset">The ruleset to get the user's info for.</param>
|
/// <param name="ruleset">The ruleset to get the user's info for.</param>
|
||||||
public GetUserRequest(long? userId = null, RulesetInfo ruleset = null)
|
public GetUserRequest(long? userId = null, RulesetInfo ruleset = null)
|
||||||
{
|
{
|
||||||
this.userIdentifier = userId.ToString();
|
lookup = userId.ToString();
|
||||||
|
lookupType = LookupType.Id;
|
||||||
Ruleset = ruleset;
|
Ruleset = ruleset;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,10 +38,17 @@ namespace osu.Game.Online.API.Requests
|
|||||||
/// <param name="ruleset">The ruleset to get the user's info for.</param>
|
/// <param name="ruleset">The ruleset to get the user's info for.</param>
|
||||||
public GetUserRequest(string username = null, RulesetInfo ruleset = null)
|
public GetUserRequest(string username = null, RulesetInfo ruleset = null)
|
||||||
{
|
{
|
||||||
this.userIdentifier = username;
|
lookup = username;
|
||||||
|
lookupType = LookupType.Username;
|
||||||
Ruleset = ruleset;
|
Ruleset = ruleset;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override string Target => userIdentifier != null ? $@"users/{userIdentifier}/{Ruleset?.ShortName}" : $@"me/{Ruleset?.ShortName}";
|
protected override string Target => lookup != null ? $@"users/{lookup}/{Ruleset?.ShortName}?k={lookupType.ToString().ToLower()}" : $@"me/{Ruleset?.ShortName}";
|
||||||
|
|
||||||
|
private enum LookupType
|
||||||
|
{
|
||||||
|
Id,
|
||||||
|
Username
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Humanizer;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.IO.Network;
|
using osu.Framework.IO.Network;
|
||||||
using osu.Game.Extensions;
|
using osu.Game.Extensions;
|
||||||
@ -83,7 +84,7 @@ namespace osu.Game.Online.API.Requests
|
|||||||
req.AddParameter("q", query);
|
req.AddParameter("q", query);
|
||||||
|
|
||||||
if (General != null && General.Any())
|
if (General != null && General.Any())
|
||||||
req.AddParameter("c", string.Join('.', General.Select(e => e.ToString().ToLowerInvariant())));
|
req.AddParameter("c", string.Join('.', General.Select(e => e.ToString().Underscore())));
|
||||||
|
|
||||||
if (ruleset.ID.HasValue)
|
if (ruleset.ID.HasValue)
|
||||||
req.AddParameter("m", ruleset.ID.Value.ToString());
|
req.AddParameter("m", ruleset.ID.Value.ToString());
|
||||||
|
@ -34,6 +34,8 @@ namespace osu.Game.Online.Leaderboards
|
|||||||
{
|
{
|
||||||
public const float HEIGHT = 60;
|
public const float HEIGHT = 60;
|
||||||
|
|
||||||
|
public readonly ScoreInfo Score;
|
||||||
|
|
||||||
private const float corner_radius = 5;
|
private const float corner_radius = 5;
|
||||||
private const float edge_margin = 5;
|
private const float edge_margin = 5;
|
||||||
private const float background_alpha = 0.25f;
|
private const float background_alpha = 0.25f;
|
||||||
@ -41,7 +43,6 @@ namespace osu.Game.Online.Leaderboards
|
|||||||
|
|
||||||
protected Container RankContainer { get; private set; }
|
protected Container RankContainer { get; private set; }
|
||||||
|
|
||||||
private readonly ScoreInfo score;
|
|
||||||
private readonly int? rank;
|
private readonly int? rank;
|
||||||
private readonly bool allowHighlight;
|
private readonly bool allowHighlight;
|
||||||
|
|
||||||
@ -67,7 +68,8 @@ namespace osu.Game.Online.Leaderboards
|
|||||||
|
|
||||||
public LeaderboardScore(ScoreInfo score, int? rank, bool allowHighlight = true)
|
public LeaderboardScore(ScoreInfo score, int? rank, bool allowHighlight = true)
|
||||||
{
|
{
|
||||||
this.score = score;
|
Score = score;
|
||||||
|
|
||||||
this.rank = rank;
|
this.rank = rank;
|
||||||
this.allowHighlight = allowHighlight;
|
this.allowHighlight = allowHighlight;
|
||||||
|
|
||||||
@ -78,9 +80,9 @@ namespace osu.Game.Online.Leaderboards
|
|||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(IAPIProvider api, OsuColour colour, ScoreManager scoreManager)
|
private void load(IAPIProvider api, OsuColour colour, ScoreManager scoreManager)
|
||||||
{
|
{
|
||||||
var user = score.User;
|
var user = Score.User;
|
||||||
|
|
||||||
statisticsLabels = GetStatistics(score).Select(s => new ScoreComponentLabel(s)).ToList();
|
statisticsLabels = GetStatistics(Score).Select(s => new ScoreComponentLabel(s)).ToList();
|
||||||
|
|
||||||
ClickableAvatar innerAvatar;
|
ClickableAvatar innerAvatar;
|
||||||
|
|
||||||
@ -198,7 +200,7 @@ namespace osu.Game.Online.Leaderboards
|
|||||||
{
|
{
|
||||||
TextColour = Color4.White,
|
TextColour = Color4.White,
|
||||||
GlowColour = Color4Extensions.FromHex(@"83ccfa"),
|
GlowColour = Color4Extensions.FromHex(@"83ccfa"),
|
||||||
Current = scoreManager.GetBindableTotalScoreString(score),
|
Current = scoreManager.GetBindableTotalScoreString(Score),
|
||||||
Font = OsuFont.Numeric.With(size: 23),
|
Font = OsuFont.Numeric.With(size: 23),
|
||||||
},
|
},
|
||||||
RankContainer = new Container
|
RankContainer = new Container
|
||||||
@ -206,7 +208,7 @@ namespace osu.Game.Online.Leaderboards
|
|||||||
Size = new Vector2(40f, 20f),
|
Size = new Vector2(40f, 20f),
|
||||||
Children = new[]
|
Children = new[]
|
||||||
{
|
{
|
||||||
scoreRank = new UpdateableRank(score.Rank)
|
scoreRank = new UpdateableRank(Score.Rank)
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
@ -223,7 +225,7 @@ namespace osu.Game.Online.Leaderboards
|
|||||||
AutoSizeAxes = Axes.Both,
|
AutoSizeAxes = Axes.Both,
|
||||||
Direction = FillDirection.Horizontal,
|
Direction = FillDirection.Horizontal,
|
||||||
Spacing = new Vector2(1),
|
Spacing = new Vector2(1),
|
||||||
ChildrenEnumerable = score.Mods.Select(mod => new ModIcon(mod) { Scale = new Vector2(0.375f) })
|
ChildrenEnumerable = Score.Mods.Select(mod => new ModIcon(mod) { Scale = new Vector2(0.375f) })
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -389,14 +391,14 @@ namespace osu.Game.Online.Leaderboards
|
|||||||
{
|
{
|
||||||
List<MenuItem> items = new List<MenuItem>();
|
List<MenuItem> items = new List<MenuItem>();
|
||||||
|
|
||||||
if (score.Mods.Length > 0 && modsContainer.Any(s => s.IsHovered) && songSelect != null)
|
if (Score.Mods.Length > 0 && modsContainer.Any(s => s.IsHovered) && songSelect != null)
|
||||||
items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = score.Mods));
|
items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = Score.Mods));
|
||||||
|
|
||||||
if (score.Files?.Count > 0)
|
if (Score.Files?.Count > 0)
|
||||||
items.Add(new OsuMenuItem("Export", MenuItemType.Standard, () => scoreManager.Export(score)));
|
items.Add(new OsuMenuItem("Export", MenuItemType.Standard, () => scoreManager.Export(Score)));
|
||||||
|
|
||||||
if (score.ID != 0)
|
if (Score.ID != 0)
|
||||||
items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(score))));
|
items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(Score))));
|
||||||
|
|
||||||
return items.ToArray();
|
return items.ToArray();
|
||||||
}
|
}
|
||||||
|
@ -205,31 +205,7 @@ namespace osu.Game
|
|||||||
dependencies.CacheAs(this);
|
dependencies.CacheAs(this);
|
||||||
dependencies.CacheAs(LocalConfig);
|
dependencies.CacheAs(LocalConfig);
|
||||||
|
|
||||||
AddFont(Resources, @"Fonts/osuFont");
|
InitialiseFonts();
|
||||||
|
|
||||||
AddFont(Resources, @"Fonts/Torus/Torus-Regular");
|
|
||||||
AddFont(Resources, @"Fonts/Torus/Torus-Light");
|
|
||||||
AddFont(Resources, @"Fonts/Torus/Torus-SemiBold");
|
|
||||||
AddFont(Resources, @"Fonts/Torus/Torus-Bold");
|
|
||||||
|
|
||||||
AddFont(Resources, @"Fonts/Inter/Inter-Regular");
|
|
||||||
AddFont(Resources, @"Fonts/Inter/Inter-RegularItalic");
|
|
||||||
AddFont(Resources, @"Fonts/Inter/Inter-Light");
|
|
||||||
AddFont(Resources, @"Fonts/Inter/Inter-LightItalic");
|
|
||||||
AddFont(Resources, @"Fonts/Inter/Inter-SemiBold");
|
|
||||||
AddFont(Resources, @"Fonts/Inter/Inter-SemiBoldItalic");
|
|
||||||
AddFont(Resources, @"Fonts/Inter/Inter-Bold");
|
|
||||||
AddFont(Resources, @"Fonts/Inter/Inter-BoldItalic");
|
|
||||||
|
|
||||||
AddFont(Resources, @"Fonts/Noto/Noto-Basic");
|
|
||||||
AddFont(Resources, @"Fonts/Noto/Noto-Hangul");
|
|
||||||
AddFont(Resources, @"Fonts/Noto/Noto-CJK-Basic");
|
|
||||||
AddFont(Resources, @"Fonts/Noto/Noto-CJK-Compatibility");
|
|
||||||
AddFont(Resources, @"Fonts/Noto/Noto-Thai");
|
|
||||||
|
|
||||||
AddFont(Resources, @"Fonts/Venera/Venera-Light");
|
|
||||||
AddFont(Resources, @"Fonts/Venera/Venera-Bold");
|
|
||||||
AddFont(Resources, @"Fonts/Venera/Venera-Black");
|
|
||||||
|
|
||||||
Audio.Samples.PlaybackConcurrency = SAMPLE_CONCURRENCY;
|
Audio.Samples.PlaybackConcurrency = SAMPLE_CONCURRENCY;
|
||||||
|
|
||||||
@ -267,7 +243,7 @@ namespace osu.Game
|
|||||||
dependencies.Cache(fileStore = new FileStore(contextFactory, Storage));
|
dependencies.Cache(fileStore = new FileStore(contextFactory, Storage));
|
||||||
|
|
||||||
// ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup()
|
// ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup()
|
||||||
dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, API, contextFactory, Host, () => difficultyCache, LocalConfig));
|
dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, API, contextFactory, Scheduler, Host, () => difficultyCache, LocalConfig));
|
||||||
dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, contextFactory, RulesetStore, API, Audio, Resources, Host, defaultBeatmap, true));
|
dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, contextFactory, RulesetStore, API, Audio, Resources, Host, defaultBeatmap, true));
|
||||||
|
|
||||||
// this should likely be moved to ArchiveModelManager when another case appears where it is necessary
|
// this should likely be moved to ArchiveModelManager when another case appears where it is necessary
|
||||||
@ -351,10 +327,7 @@ namespace osu.Game
|
|||||||
base.Content.Add(CreateScalingContainer().WithChildren(mainContent));
|
base.Content.Add(CreateScalingContainer().WithChildren(mainContent));
|
||||||
|
|
||||||
KeyBindingStore = new RealmKeyBindingStore(realmFactory);
|
KeyBindingStore = new RealmKeyBindingStore(realmFactory);
|
||||||
KeyBindingStore.Register(globalBindings);
|
KeyBindingStore.Register(globalBindings, RulesetStore.AvailableRulesets);
|
||||||
|
|
||||||
foreach (var r in RulesetStore.AvailableRulesets)
|
|
||||||
KeyBindingStore.Register(r);
|
|
||||||
|
|
||||||
dependencies.Cache(globalBindings);
|
dependencies.Cache(globalBindings);
|
||||||
|
|
||||||
@ -368,6 +341,35 @@ namespace osu.Game
|
|||||||
Ruleset.BindValueChanged(onRulesetChanged);
|
Ruleset.BindValueChanged(onRulesetChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected virtual void InitialiseFonts()
|
||||||
|
{
|
||||||
|
AddFont(Resources, @"Fonts/osuFont");
|
||||||
|
|
||||||
|
AddFont(Resources, @"Fonts/Torus/Torus-Regular");
|
||||||
|
AddFont(Resources, @"Fonts/Torus/Torus-Light");
|
||||||
|
AddFont(Resources, @"Fonts/Torus/Torus-SemiBold");
|
||||||
|
AddFont(Resources, @"Fonts/Torus/Torus-Bold");
|
||||||
|
|
||||||
|
AddFont(Resources, @"Fonts/Inter/Inter-Regular");
|
||||||
|
AddFont(Resources, @"Fonts/Inter/Inter-RegularItalic");
|
||||||
|
AddFont(Resources, @"Fonts/Inter/Inter-Light");
|
||||||
|
AddFont(Resources, @"Fonts/Inter/Inter-LightItalic");
|
||||||
|
AddFont(Resources, @"Fonts/Inter/Inter-SemiBold");
|
||||||
|
AddFont(Resources, @"Fonts/Inter/Inter-SemiBoldItalic");
|
||||||
|
AddFont(Resources, @"Fonts/Inter/Inter-Bold");
|
||||||
|
AddFont(Resources, @"Fonts/Inter/Inter-BoldItalic");
|
||||||
|
|
||||||
|
AddFont(Resources, @"Fonts/Noto/Noto-Basic");
|
||||||
|
AddFont(Resources, @"Fonts/Noto/Noto-Hangul");
|
||||||
|
AddFont(Resources, @"Fonts/Noto/Noto-CJK-Basic");
|
||||||
|
AddFont(Resources, @"Fonts/Noto/Noto-CJK-Compatibility");
|
||||||
|
AddFont(Resources, @"Fonts/Noto/Noto-Thai");
|
||||||
|
|
||||||
|
AddFont(Resources, @"Fonts/Venera/Venera-Light");
|
||||||
|
AddFont(Resources, @"Fonts/Venera/Venera-Bold");
|
||||||
|
AddFont(Resources, @"Fonts/Venera/Venera-Black");
|
||||||
|
}
|
||||||
|
|
||||||
private IDisposable blocking;
|
private IDisposable blocking;
|
||||||
|
|
||||||
private void updateThreadStateChanged(ValueChangedEvent<GameThreadState> state)
|
private void updateThreadStateChanged(ValueChangedEvent<GameThreadState> state)
|
||||||
|
@ -127,7 +127,7 @@ namespace osu.Game.Overlays.BeatmapListing
|
|||||||
Padding = new MarginPadding { Horizontal = 10 },
|
Padding = new MarginPadding { Horizontal = 10 },
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
generalFilter = new BeatmapSearchMultipleSelectionFilterRow<SearchGeneral>(BeatmapsStrings.ListingSearchFiltersGeneral),
|
generalFilter = new BeatmapSearchGeneralFilterRow(),
|
||||||
modeFilter = new BeatmapSearchRulesetFilterRow(),
|
modeFilter = new BeatmapSearchRulesetFilterRow(),
|
||||||
categoryFilter = new BeatmapSearchFilterRow<SearchCategory>(BeatmapsStrings.ListingSearchFiltersStatus),
|
categoryFilter = new BeatmapSearchFilterRow<SearchCategory>(BeatmapsStrings.ListingSearchFiltersStatus),
|
||||||
genreFilter = new BeatmapSearchFilterRow<SearchGenre>(BeatmapsStrings.ListingSearchFiltersGenre),
|
genreFilter = new BeatmapSearchFilterRow<SearchGenre>(BeatmapsStrings.ListingSearchFiltersGenre),
|
||||||
|
@ -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()
|
private void updateState()
|
||||||
{
|
{
|
||||||
text.FadeColour(IsHovered ? colourProvider.Light1 : getStateColour(), 200, Easing.OutQuint);
|
text.FadeColour(IsHovered ? colourProvider.Light1 : GetStateColour(), 200, Easing.OutQuint);
|
||||||
text.Font = text.Font.With(weight: Active.Value ? FontWeight.SemiBold : FontWeight.Regular);
|
text.Font = text.Font.With(weight: Active.Value ? FontWeight.SemiBold : FontWeight.Regular);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Color4 getStateColour() => Active.Value ? colourProvider.Content1 : colourProvider.Light2;
|
protected virtual Color4 GetStateColour() => Active.Value ? colourProvider.Content1 : colourProvider.Light2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,10 @@ namespace osu.Game.Overlays.BeatmapListing
|
|||||||
|
|
||||||
[LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.GeneralFollows))]
|
[LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.GeneralFollows))]
|
||||||
[Description("Subscribed mappers")]
|
[Description("Subscribed mappers")]
|
||||||
Follows
|
Follows,
|
||||||
|
|
||||||
|
[LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.GeneralFeaturedArtists))]
|
||||||
|
[Description("Featured artists")]
|
||||||
|
FeaturedArtists
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,8 @@ using osu.Framework.Graphics.Containers;
|
|||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
@ -14,6 +16,7 @@ using osu.Game.Online.API.Requests;
|
|||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
|
using osu.Game.Scoring;
|
||||||
using osu.Game.Screens.Select.Leaderboards;
|
using osu.Game.Screens.Select.Leaderboards;
|
||||||
using osu.Game.Users;
|
using osu.Game.Users;
|
||||||
|
|
||||||
@ -42,34 +45,46 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private RulesetStore rulesets { get; set; }
|
private RulesetStore rulesets { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private ScoreManager scoreManager { get; set; }
|
||||||
|
|
||||||
private GetScoresRequest getScoresRequest;
|
private GetScoresRequest getScoresRequest;
|
||||||
|
|
||||||
|
private CancellationTokenSource loadCancellationSource;
|
||||||
|
|
||||||
protected APILegacyScores Scores
|
protected APILegacyScores Scores
|
||||||
{
|
{
|
||||||
set => Schedule(() =>
|
set => Schedule(() =>
|
||||||
{
|
{
|
||||||
|
loadCancellationSource?.Cancel();
|
||||||
|
loadCancellationSource = new CancellationTokenSource();
|
||||||
|
|
||||||
topScoresContainer.Clear();
|
topScoresContainer.Clear();
|
||||||
|
scoreTable.ClearScores();
|
||||||
|
scoreTable.Hide();
|
||||||
|
|
||||||
if (value?.Scores.Any() != true)
|
if (value?.Scores.Any() != true)
|
||||||
{
|
|
||||||
scoreTable.ClearScores();
|
|
||||||
scoreTable.Hide();
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
var scoreInfos = value.Scores.Select(s => s.CreateScoreInfo(rulesets)).ToList();
|
scoreManager.OrderByTotalScoreAsync(value.Scores.Select(s => s.CreateScoreInfo(rulesets)).ToArray(), loadCancellationSource.Token)
|
||||||
var topScore = scoreInfos.First();
|
.ContinueWith(ordered => Schedule(() =>
|
||||||
|
{
|
||||||
|
if (loadCancellationSource.IsCancellationRequested)
|
||||||
|
return;
|
||||||
|
|
||||||
scoreTable.DisplayScores(scoreInfos, topScore.Beatmap?.Status.GrantsPerformancePoints() == true);
|
var topScore = ordered.Result.First();
|
||||||
scoreTable.Show();
|
|
||||||
|
|
||||||
var userScore = value.UserScore;
|
scoreTable.DisplayScores(ordered.Result, topScore.Beatmap?.Status.GrantsPerformancePoints() == true);
|
||||||
var userScoreInfo = userScore?.Score.CreateScoreInfo(rulesets);
|
scoreTable.Show();
|
||||||
|
|
||||||
topScoresContainer.Add(new DrawableTopScore(topScore));
|
var userScore = value.UserScore;
|
||||||
|
var userScoreInfo = userScore?.Score.CreateScoreInfo(rulesets);
|
||||||
|
|
||||||
if (userScoreInfo != null && userScoreInfo.OnlineScoreID != topScore.OnlineScoreID)
|
topScoresContainer.Add(new DrawableTopScore(topScore));
|
||||||
topScoresContainer.Add(new DrawableTopScore(userScoreInfo, userScore.Position));
|
|
||||||
|
if (userScoreInfo != null && userScoreInfo.OnlineScoreID != topScore.OnlineScoreID)
|
||||||
|
topScoresContainer.Add(new DrawableTopScore(userScoreInfo, userScore.Position));
|
||||||
|
}), TaskContinuationOptions.OnlyOnRanToCompletion);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,10 +4,13 @@
|
|||||||
using System;
|
using System;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Extensions.MatrixExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Primitives;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Framework.Input.Handlers.Tablet;
|
using osu.Framework.Input.Handlers.Tablet;
|
||||||
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
@ -17,6 +20,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
|||||||
{
|
{
|
||||||
public class TabletAreaSelection : CompositeDrawable
|
public class TabletAreaSelection : CompositeDrawable
|
||||||
{
|
{
|
||||||
|
public bool IsWithinBounds { get; private set; }
|
||||||
|
|
||||||
private readonly ITabletHandler handler;
|
private readonly ITabletHandler handler;
|
||||||
|
|
||||||
private Container tabletContainer;
|
private Container tabletContainer;
|
||||||
@ -109,29 +114,30 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
|||||||
areaOffset.BindTo(handler.AreaOffset);
|
areaOffset.BindTo(handler.AreaOffset);
|
||||||
areaOffset.BindValueChanged(val =>
|
areaOffset.BindValueChanged(val =>
|
||||||
{
|
{
|
||||||
usableAreaContainer.MoveTo(val.NewValue, 100, Easing.OutQuint)
|
usableAreaContainer.MoveTo(val.NewValue, 100, Easing.OutQuint);
|
||||||
.OnComplete(_ => checkBounds()); // required as we are using SSDQ.
|
checkBounds();
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
areaSize.BindTo(handler.AreaSize);
|
areaSize.BindTo(handler.AreaSize);
|
||||||
areaSize.BindValueChanged(val =>
|
areaSize.BindValueChanged(val =>
|
||||||
{
|
{
|
||||||
usableAreaContainer.ResizeTo(val.NewValue, 100, Easing.OutQuint)
|
usableAreaContainer.ResizeTo(val.NewValue, 100, Easing.OutQuint);
|
||||||
.OnComplete(_ => checkBounds()); // required as we are using SSDQ.
|
|
||||||
|
|
||||||
int x = (int)val.NewValue.X;
|
int x = (int)val.NewValue.X;
|
||||||
int y = (int)val.NewValue.Y;
|
int y = (int)val.NewValue.Y;
|
||||||
int commonDivider = greatestCommonDivider(x, y);
|
int commonDivider = greatestCommonDivider(x, y);
|
||||||
|
|
||||||
usableAreaText.Text = $"{(float)x / commonDivider}:{(float)y / commonDivider}";
|
usableAreaText.Text = $"{(float)x / commonDivider}:{(float)y / commonDivider}";
|
||||||
|
checkBounds();
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
rotation.BindTo(handler.Rotation);
|
rotation.BindTo(handler.Rotation);
|
||||||
rotation.BindValueChanged(val =>
|
rotation.BindValueChanged(val =>
|
||||||
{
|
{
|
||||||
|
usableAreaContainer.RotateTo(val.NewValue, 100, Easing.OutQuint);
|
||||||
tabletContainer.RotateTo(-val.NewValue, 800, Easing.OutQuint);
|
tabletContainer.RotateTo(-val.NewValue, 800, Easing.OutQuint);
|
||||||
usableAreaContainer.RotateTo(val.NewValue, 100, Easing.OutQuint)
|
|
||||||
.OnComplete(_ => checkBounds()); // required as we are using SSDQ.
|
checkBounds();
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
tablet.BindTo(handler.Tablet);
|
tablet.BindTo(handler.Tablet);
|
||||||
@ -169,12 +175,35 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
|||||||
if (tablet.Value == null)
|
if (tablet.Value == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var usableSsdq = usableAreaContainer.ScreenSpaceDrawQuad;
|
// allow for some degree of floating point error, as we don't care about being perfect here.
|
||||||
|
const float lenience = 0.5f;
|
||||||
|
|
||||||
bool isWithinBounds = tabletContainer.ScreenSpaceDrawQuad.Contains(usableSsdq.TopLeft + new Vector2(1)) &&
|
var tabletArea = new Quad(-lenience, -lenience, tablet.Value.Size.X + lenience * 2, tablet.Value.Size.Y + lenience * 2);
|
||||||
tabletContainer.ScreenSpaceDrawQuad.Contains(usableSsdq.BottomRight - new Vector2(1));
|
|
||||||
|
|
||||||
usableFill.FadeColour(isWithinBounds ? colour.Blue : colour.RedLight, 100);
|
var halfUsableArea = areaSize.Value / 2;
|
||||||
|
var offset = areaOffset.Value;
|
||||||
|
|
||||||
|
var usableAreaQuad = new Quad(
|
||||||
|
new Vector2(-halfUsableArea.X, -halfUsableArea.Y),
|
||||||
|
new Vector2(halfUsableArea.X, -halfUsableArea.Y),
|
||||||
|
new Vector2(-halfUsableArea.X, halfUsableArea.Y),
|
||||||
|
new Vector2(halfUsableArea.X, halfUsableArea.Y)
|
||||||
|
);
|
||||||
|
|
||||||
|
var matrix = Matrix3.Identity;
|
||||||
|
|
||||||
|
MatrixExtensions.TranslateFromLeft(ref matrix, offset);
|
||||||
|
MatrixExtensions.RotateFromLeft(ref matrix, MathUtils.DegreesToRadians(rotation.Value));
|
||||||
|
|
||||||
|
usableAreaQuad *= matrix;
|
||||||
|
|
||||||
|
IsWithinBounds =
|
||||||
|
tabletArea.Contains(usableAreaQuad.TopLeft) &&
|
||||||
|
tabletArea.Contains(usableAreaQuad.TopRight) &&
|
||||||
|
tabletArea.Contains(usableAreaQuad.BottomLeft) &&
|
||||||
|
tabletArea.Contains(usableAreaQuad.BottomRight);
|
||||||
|
|
||||||
|
usableFill.FadeColour(IsWithinBounds ? colour.Blue : colour.RedLight, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
|
@ -20,6 +20,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
|||||||
{
|
{
|
||||||
public class TabletSettings : SettingsSubsection
|
public class TabletSettings : SettingsSubsection
|
||||||
{
|
{
|
||||||
|
public TabletAreaSelection AreaSelection { get; private set; }
|
||||||
|
|
||||||
private readonly ITabletHandler tabletHandler;
|
private readonly ITabletHandler tabletHandler;
|
||||||
|
|
||||||
private readonly Bindable<bool> enabled = new BindableBool(true);
|
private readonly Bindable<bool> enabled = new BindableBool(true);
|
||||||
@ -121,7 +123,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
|||||||
Direction = FillDirection.Vertical,
|
Direction = FillDirection.Vertical,
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
new TabletAreaSelection(tabletHandler)
|
AreaSelection = new TabletAreaSelection(tabletHandler)
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
Height = 300,
|
Height = 300,
|
||||||
|
@ -13,6 +13,7 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Logging;
|
using osu.Framework.Logging;
|
||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
|
using osu.Framework.Threading;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
@ -36,6 +37,7 @@ namespace osu.Game.Scoring
|
|||||||
|
|
||||||
private readonly RulesetStore rulesets;
|
private readonly RulesetStore rulesets;
|
||||||
private readonly Func<BeatmapManager> beatmaps;
|
private readonly Func<BeatmapManager> beatmaps;
|
||||||
|
private readonly Scheduler scheduler;
|
||||||
|
|
||||||
[CanBeNull]
|
[CanBeNull]
|
||||||
private readonly Func<BeatmapDifficultyCache> difficulties;
|
private readonly Func<BeatmapDifficultyCache> difficulties;
|
||||||
@ -43,12 +45,13 @@ namespace osu.Game.Scoring
|
|||||||
[CanBeNull]
|
[CanBeNull]
|
||||||
private readonly OsuConfigManager configManager;
|
private readonly OsuConfigManager configManager;
|
||||||
|
|
||||||
public ScoreManager(RulesetStore rulesets, Func<BeatmapManager> beatmaps, Storage storage, IAPIProvider api, IDatabaseContextFactory contextFactory, IIpcHost importHost = null,
|
public ScoreManager(RulesetStore rulesets, Func<BeatmapManager> beatmaps, Storage storage, IAPIProvider api, IDatabaseContextFactory contextFactory, Scheduler scheduler,
|
||||||
Func<BeatmapDifficultyCache> difficulties = null, OsuConfigManager configManager = null)
|
IIpcHost importHost = null, Func<BeatmapDifficultyCache> difficulties = null, OsuConfigManager configManager = null)
|
||||||
: base(storage, contextFactory, api, new ScoreStore(contextFactory, storage), importHost)
|
: base(storage, contextFactory, api, new ScoreStore(contextFactory, storage), importHost)
|
||||||
{
|
{
|
||||||
this.rulesets = rulesets;
|
this.rulesets = rulesets;
|
||||||
this.beatmaps = beatmaps;
|
this.beatmaps = beatmaps;
|
||||||
|
this.scheduler = scheduler;
|
||||||
this.difficulties = difficulties;
|
this.difficulties = difficulties;
|
||||||
this.configManager = configManager;
|
this.configManager = configManager;
|
||||||
}
|
}
|
||||||
@ -103,6 +106,32 @@ namespace osu.Game.Scoring
|
|||||||
=> base.CheckLocalAvailability(model, items)
|
=> base.CheckLocalAvailability(model, items)
|
||||||
|| (model.OnlineScoreID != null && items.Any(i => i.OnlineScoreID == model.OnlineScoreID));
|
|| (model.OnlineScoreID != null && items.Any(i => i.OnlineScoreID == model.OnlineScoreID));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Orders an array of <see cref="ScoreInfo"/>s by total score.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="scores">The array of <see cref="ScoreInfo"/>s to reorder.</param>
|
||||||
|
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to cancel the process.</param>
|
||||||
|
/// <returns>The given <paramref name="scores"/> ordered by decreasing total score.</returns>
|
||||||
|
public async Task<ScoreInfo[]> OrderByTotalScoreAsync(ScoreInfo[] scores, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var difficultyCache = difficulties?.Invoke();
|
||||||
|
|
||||||
|
if (difficultyCache != null)
|
||||||
|
{
|
||||||
|
// Compute difficulties asynchronously first to prevent blocking via the GetTotalScore() call below.
|
||||||
|
foreach (var s in scores)
|
||||||
|
{
|
||||||
|
await difficultyCache.GetDifficultyAsync(s.Beatmap, s.Ruleset, s.Mods, cancellationToken).ConfigureAwait(false);
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're calling .Result, but this should not be a blocking call due to the above GetDifficultyAsync() calls.
|
||||||
|
return scores.OrderByDescending(s => GetTotalScoreAsync(s, cancellationToken: cancellationToken).Result)
|
||||||
|
.ThenBy(s => s.OnlineScoreID)
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves a bindable that represents the total score of a <see cref="ScoreInfo"/>.
|
/// Retrieves a bindable that represents the total score of a <see cref="ScoreInfo"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -111,9 +140,9 @@ namespace osu.Game.Scoring
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
/// <param name="score">The <see cref="ScoreInfo"/> to retrieve the bindable for.</param>
|
/// <param name="score">The <see cref="ScoreInfo"/> to retrieve the bindable for.</param>
|
||||||
/// <returns>The bindable containing the total score.</returns>
|
/// <returns>The bindable containing the total score.</returns>
|
||||||
public Bindable<long> GetBindableTotalScore(ScoreInfo score)
|
public Bindable<long> GetBindableTotalScore([NotNull] ScoreInfo score)
|
||||||
{
|
{
|
||||||
var bindable = new TotalScoreBindable(score, difficulties);
|
var bindable = new TotalScoreBindable(score, this);
|
||||||
configManager?.BindWith(OsuSetting.ScoreDisplayMode, bindable.ScoringMode);
|
configManager?.BindWith(OsuSetting.ScoreDisplayMode, bindable.ScoringMode);
|
||||||
return bindable;
|
return bindable;
|
||||||
}
|
}
|
||||||
@ -126,7 +155,83 @@ namespace osu.Game.Scoring
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
/// <param name="score">The <see cref="ScoreInfo"/> to retrieve the bindable for.</param>
|
/// <param name="score">The <see cref="ScoreInfo"/> to retrieve the bindable for.</param>
|
||||||
/// <returns>The bindable containing the formatted total score string.</returns>
|
/// <returns>The bindable containing the formatted total score string.</returns>
|
||||||
public Bindable<string> GetBindableTotalScoreString(ScoreInfo score) => new TotalScoreStringBindable(GetBindableTotalScore(score));
|
public Bindable<string> GetBindableTotalScoreString([NotNull] ScoreInfo score) => new TotalScoreStringBindable(GetBindableTotalScore(score));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the total score of a <see cref="ScoreInfo"/> in the given <see cref="ScoringMode"/>.
|
||||||
|
/// The score is returned in a callback that is run on the update thread.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="score">The <see cref="ScoreInfo"/> to calculate the total score of.</param>
|
||||||
|
/// <param name="callback">The callback to be invoked with the total score.</param>
|
||||||
|
/// <param name="mode">The <see cref="ScoringMode"/> to return the total score as.</param>
|
||||||
|
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to cancel the process.</param>
|
||||||
|
public void GetTotalScore([NotNull] ScoreInfo score, [NotNull] Action<long> callback, ScoringMode mode = ScoringMode.Standardised, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
GetTotalScoreAsync(score, mode, cancellationToken)
|
||||||
|
.ContinueWith(s => scheduler.Add(() => callback(s.Result)), TaskContinuationOptions.OnlyOnRanToCompletion);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the total score of a <see cref="ScoreInfo"/> in the given <see cref="ScoringMode"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="score">The <see cref="ScoreInfo"/> to calculate the total score of.</param>
|
||||||
|
/// <param name="mode">The <see cref="ScoringMode"/> to return the total score as.</param>
|
||||||
|
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to cancel the process.</param>
|
||||||
|
/// <returns>The total score.</returns>
|
||||||
|
public async Task<long> GetTotalScoreAsync([NotNull] ScoreInfo score, ScoringMode mode = ScoringMode.Standardised, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (score.Beatmap == null)
|
||||||
|
return score.TotalScore;
|
||||||
|
|
||||||
|
int beatmapMaxCombo;
|
||||||
|
double accuracy = score.Accuracy;
|
||||||
|
|
||||||
|
if (score.IsLegacyScore)
|
||||||
|
{
|
||||||
|
if (score.RulesetID == 3)
|
||||||
|
{
|
||||||
|
// In osu!stable, a full-GREAT score has 100% accuracy in mania. Along with a full combo, the score becomes indistinguishable from a full-PERFECT score.
|
||||||
|
// To get around this, recalculate accuracy based on the hit statistics.
|
||||||
|
// Note: This cannot be applied universally to all legacy scores, as some rulesets (e.g. catch) group multiple judgements together.
|
||||||
|
double maxBaseScore = score.Statistics.Select(kvp => kvp.Value).Sum() * Judgement.ToNumericResult(HitResult.Perfect);
|
||||||
|
double baseScore = score.Statistics.Select(kvp => Judgement.ToNumericResult(kvp.Key) * kvp.Value).Sum();
|
||||||
|
if (maxBaseScore > 0)
|
||||||
|
accuracy = baseScore / maxBaseScore;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This score is guaranteed to be an osu!stable score.
|
||||||
|
// The combo must be determined through either the beatmap's max combo value or the difficulty calculator, as lazer's scoring has changed and the score statistics cannot be used.
|
||||||
|
if (score.Beatmap.MaxCombo != null)
|
||||||
|
beatmapMaxCombo = score.Beatmap.MaxCombo.Value;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (score.Beatmap.ID == 0 || difficulties == null)
|
||||||
|
{
|
||||||
|
// We don't have enough information (max combo) to compute the score, so use the provided score.
|
||||||
|
return score.TotalScore;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can compute the max combo locally after the async beatmap difficulty computation.
|
||||||
|
var difficulty = await difficulties().GetDifficultyAsync(score.Beatmap, score.Ruleset, score.Mods, cancellationToken).ConfigureAwait(false);
|
||||||
|
beatmapMaxCombo = difficulty.MaxCombo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// This is guaranteed to be a non-legacy score.
|
||||||
|
// The combo must be determined through the score's statistics, as both the beatmap's max combo and the difficulty calculator will provide osu!stable combo values.
|
||||||
|
beatmapMaxCombo = Enum.GetValues(typeof(HitResult)).OfType<HitResult>().Where(r => r.AffectsCombo()).Select(r => score.Statistics.GetValueOrDefault(r)).Sum();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (beatmapMaxCombo == 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
var ruleset = score.Ruleset.CreateInstance();
|
||||||
|
var scoreProcessor = ruleset.CreateScoreProcessor();
|
||||||
|
scoreProcessor.Mods.Value = score.Mods;
|
||||||
|
|
||||||
|
return (long)Math.Round(scoreProcessor.GetScore(mode, beatmapMaxCombo, accuracy, (double)score.MaxCombo / beatmapMaxCombo, score.Statistics));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides the total score of a <see cref="ScoreInfo"/>. Responds to changes in the currently-selected <see cref="ScoringMode"/>.
|
/// Provides the total score of a <see cref="ScoreInfo"/>. Responds to changes in the currently-selected <see cref="ScoringMode"/>.
|
||||||
@ -136,99 +241,29 @@ namespace osu.Game.Scoring
|
|||||||
public readonly Bindable<ScoringMode> ScoringMode = new Bindable<ScoringMode>();
|
public readonly Bindable<ScoringMode> ScoringMode = new Bindable<ScoringMode>();
|
||||||
|
|
||||||
private readonly ScoreInfo score;
|
private readonly ScoreInfo score;
|
||||||
private readonly Func<BeatmapDifficultyCache> difficulties;
|
private readonly ScoreManager scoreManager;
|
||||||
|
|
||||||
|
private CancellationTokenSource difficultyCalculationCancellationSource;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new <see cref="TotalScoreBindable"/>.
|
/// Creates a new <see cref="TotalScoreBindable"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="score">The <see cref="ScoreInfo"/> to provide the total score of.</param>
|
/// <param name="score">The <see cref="ScoreInfo"/> to provide the total score of.</param>
|
||||||
/// <param name="difficulties">A function to retrieve the <see cref="BeatmapDifficultyCache"/>.</param>
|
/// <param name="scoreManager">The <see cref="ScoreManager"/>.</param>
|
||||||
public TotalScoreBindable(ScoreInfo score, Func<BeatmapDifficultyCache> difficulties)
|
public TotalScoreBindable(ScoreInfo score, ScoreManager scoreManager)
|
||||||
{
|
{
|
||||||
this.score = score;
|
this.score = score;
|
||||||
this.difficulties = difficulties;
|
this.scoreManager = scoreManager;
|
||||||
|
|
||||||
ScoringMode.BindValueChanged(onScoringModeChanged, true);
|
ScoringMode.BindValueChanged(onScoringModeChanged, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private IBindable<StarDifficulty?> difficultyBindable;
|
|
||||||
private CancellationTokenSource difficultyCancellationSource;
|
|
||||||
|
|
||||||
private void onScoringModeChanged(ValueChangedEvent<ScoringMode> mode)
|
private void onScoringModeChanged(ValueChangedEvent<ScoringMode> mode)
|
||||||
{
|
{
|
||||||
difficultyCancellationSource?.Cancel();
|
difficultyCalculationCancellationSource?.Cancel();
|
||||||
difficultyCancellationSource = null;
|
difficultyCalculationCancellationSource = new CancellationTokenSource();
|
||||||
|
|
||||||
if (score.Beatmap == null)
|
scoreManager.GetTotalScore(score, s => Value = s, mode.NewValue, difficultyCalculationCancellationSource.Token);
|
||||||
{
|
|
||||||
Value = score.TotalScore;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int beatmapMaxCombo;
|
|
||||||
double accuracy = score.Accuracy;
|
|
||||||
|
|
||||||
if (score.IsLegacyScore)
|
|
||||||
{
|
|
||||||
if (score.RulesetID == 3)
|
|
||||||
{
|
|
||||||
// In osu!stable, a full-GREAT score has 100% accuracy in mania. Along with a full combo, the score becomes indistinguishable from a full-PERFECT score.
|
|
||||||
// To get around this, recalculate accuracy based on the hit statistics.
|
|
||||||
// Note: This cannot be applied universally to all legacy scores, as some rulesets (e.g. catch) group multiple judgements together.
|
|
||||||
double maxBaseScore = score.Statistics.Select(kvp => kvp.Value).Sum() * Judgement.ToNumericResult(HitResult.Perfect);
|
|
||||||
double baseScore = score.Statistics.Select(kvp => Judgement.ToNumericResult(kvp.Key) * kvp.Value).Sum();
|
|
||||||
if (maxBaseScore > 0)
|
|
||||||
accuracy = baseScore / maxBaseScore;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This score is guaranteed to be an osu!stable score.
|
|
||||||
// The combo must be determined through either the beatmap's max combo value or the difficulty calculator, as lazer's scoring has changed and the score statistics cannot be used.
|
|
||||||
if (score.Beatmap.MaxCombo == null)
|
|
||||||
{
|
|
||||||
if (score.Beatmap.ID == 0 || difficulties == null)
|
|
||||||
{
|
|
||||||
// We don't have enough information (max combo) to compute the score, so use the provided score.
|
|
||||||
Value = score.TotalScore;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We can compute the max combo locally after the async beatmap difficulty computation.
|
|
||||||
difficultyBindable = difficulties().GetBindableDifficulty(score.Beatmap, score.Ruleset, score.Mods, (difficultyCancellationSource = new CancellationTokenSource()).Token);
|
|
||||||
difficultyBindable.BindValueChanged(d =>
|
|
||||||
{
|
|
||||||
if (d.NewValue is StarDifficulty diff)
|
|
||||||
updateScore(diff.MaxCombo, accuracy);
|
|
||||||
}, true);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
beatmapMaxCombo = score.Beatmap.MaxCombo.Value;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// This is guaranteed to be a non-legacy score.
|
|
||||||
// The combo must be determined through the score's statistics, as both the beatmap's max combo and the difficulty calculator will provide osu!stable combo values.
|
|
||||||
beatmapMaxCombo = Enum.GetValues(typeof(HitResult)).OfType<HitResult>().Where(r => r.AffectsCombo()).Select(r => score.Statistics.GetValueOrDefault(r)).Sum();
|
|
||||||
}
|
|
||||||
|
|
||||||
updateScore(beatmapMaxCombo, accuracy);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateScore(int beatmapMaxCombo, double accuracy)
|
|
||||||
{
|
|
||||||
if (beatmapMaxCombo == 0)
|
|
||||||
{
|
|
||||||
Value = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var ruleset = score.Ruleset.CreateInstance();
|
|
||||||
var scoreProcessor = ruleset.CreateScoreProcessor();
|
|
||||||
|
|
||||||
scoreProcessor.Mods.Value = score.Mods;
|
|
||||||
|
|
||||||
Value = (long)Math.Round(scoreProcessor.GetScore(ScoringMode.Value, beatmapMaxCombo, accuracy, (double)score.MaxCombo / beatmapMaxCombo, score.Statistics));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,15 +17,21 @@ namespace osu.Game.Screens
|
|||||||
Origin = Anchor.Centre;
|
Origin = Anchor.Centre;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Push(BackgroundScreen screen)
|
/// <summary>
|
||||||
|
/// Attempt to push a new background screen to this stack.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="screen">The screen to attempt to push.</param>
|
||||||
|
/// <returns>Whether the push succeeded. For example, if the existing screen was already of the correct type this will return <c>false</c>.</returns>
|
||||||
|
public bool Push(BackgroundScreen screen)
|
||||||
{
|
{
|
||||||
if (screen == null)
|
if (screen == null)
|
||||||
return;
|
return false;
|
||||||
|
|
||||||
if (EqualityComparer<BackgroundScreen>.Default.Equals((BackgroundScreen)CurrentScreen, screen))
|
if (EqualityComparer<BackgroundScreen>.Default.Equals((BackgroundScreen)CurrentScreen, screen))
|
||||||
return;
|
return false;
|
||||||
|
|
||||||
base.Push(screen);
|
base.Push(screen);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using JetBrains.Annotations;
|
||||||
using osu.Framework;
|
using osu.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
@ -75,6 +76,9 @@ namespace osu.Game.Screens.Edit
|
|||||||
|
|
||||||
private Container<EditorScreen> screenContainer;
|
private Container<EditorScreen> screenContainer;
|
||||||
|
|
||||||
|
[CanBeNull]
|
||||||
|
private readonly EditorLoader loader;
|
||||||
|
|
||||||
private EditorScreen currentScreen;
|
private EditorScreen currentScreen;
|
||||||
|
|
||||||
private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor();
|
private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor();
|
||||||
@ -101,6 +105,11 @@ namespace osu.Game.Screens.Edit
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private MusicController music { get; set; }
|
private MusicController music { get; set; }
|
||||||
|
|
||||||
|
public Editor(EditorLoader loader = null)
|
||||||
|
{
|
||||||
|
this.loader = loader;
|
||||||
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(OsuColour colours, OsuConfigManager config)
|
private void load(OsuColour colours, OsuConfigManager config)
|
||||||
{
|
{
|
||||||
@ -489,7 +498,7 @@ namespace osu.Game.Screens.Edit
|
|||||||
|
|
||||||
if (isNewBeatmap || HasUnsavedChanges)
|
if (isNewBeatmap || HasUnsavedChanges)
|
||||||
{
|
{
|
||||||
dialogOverlay?.Push(new PromptForSaveDialog(confirmExit, confirmExitWithSave));
|
dialogOverlay?.Push(new PromptForSaveDialog(confirmExit, confirmExitWithSave, cancelExit));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -703,11 +712,38 @@ namespace osu.Game.Screens.Edit
|
|||||||
if (RuntimeInfo.IsDesktop)
|
if (RuntimeInfo.IsDesktop)
|
||||||
fileMenuItems.Add(new EditorMenuItem("Export package", MenuItemType.Standard, exportBeatmap));
|
fileMenuItems.Add(new EditorMenuItem("Export package", MenuItemType.Standard, exportBeatmap));
|
||||||
|
|
||||||
|
fileMenuItems.Add(new EditorMenuItemSpacer());
|
||||||
|
|
||||||
|
var beatmapSet = beatmapManager.QueryBeatmapSet(bs => bs.ID == Beatmap.Value.BeatmapSetInfo.ID) ?? playableBeatmap.BeatmapInfo.BeatmapSet;
|
||||||
|
|
||||||
|
var difficultyItems = new List<MenuItem>();
|
||||||
|
|
||||||
|
foreach (var rulesetBeatmaps in beatmapSet.Beatmaps.GroupBy(b => b.RulesetID).OrderBy(group => group.Key))
|
||||||
|
{
|
||||||
|
if (difficultyItems.Count > 0)
|
||||||
|
difficultyItems.Add(new EditorMenuItemSpacer());
|
||||||
|
|
||||||
|
foreach (var beatmap in rulesetBeatmaps.OrderBy(b => b.StarDifficulty))
|
||||||
|
difficultyItems.Add(createDifficultyMenuItem(beatmap));
|
||||||
|
}
|
||||||
|
|
||||||
|
fileMenuItems.Add(new EditorMenuItem("Change difficulty") { Items = difficultyItems });
|
||||||
|
|
||||||
fileMenuItems.Add(new EditorMenuItemSpacer());
|
fileMenuItems.Add(new EditorMenuItemSpacer());
|
||||||
fileMenuItems.Add(new EditorMenuItem("Exit", MenuItemType.Standard, this.Exit));
|
fileMenuItems.Add(new EditorMenuItem("Exit", MenuItemType.Standard, this.Exit));
|
||||||
return fileMenuItems;
|
return fileMenuItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private DifficultyMenuItem createDifficultyMenuItem(BeatmapInfo beatmapInfo)
|
||||||
|
{
|
||||||
|
bool isCurrentDifficulty = playableBeatmap.BeatmapInfo.Equals(beatmapInfo);
|
||||||
|
return new DifficultyMenuItem(beatmapInfo, isCurrentDifficulty, switchToDifficulty);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void switchToDifficulty(BeatmapInfo beatmapInfo) => loader?.ScheduleDifficultySwitch(beatmapInfo);
|
||||||
|
|
||||||
|
private void cancelExit() => loader?.CancelPendingDifficultySwitch();
|
||||||
|
|
||||||
public double SnapTime(double time, double? referenceTime) => editorBeatmap.SnapTime(time, referenceTime);
|
public double SnapTime(double time, double? referenceTime) => editorBeatmap.SnapTime(time, referenceTime);
|
||||||
|
|
||||||
public double GetBeatLengthAtTime(double referenceTime) => editorBeatmap.GetBeatLengthAtTime(referenceTime);
|
public double GetBeatLengthAtTime(double referenceTime) => editorBeatmap.GetBeatLengthAtTime(referenceTime);
|
||||||
|
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 class PromptForSaveDialog : PopupDialog
|
||||||
{
|
{
|
||||||
public PromptForSaveDialog(Action exit, Action saveAndExit)
|
public PromptForSaveDialog(Action exit, Action saveAndExit, Action cancel)
|
||||||
{
|
{
|
||||||
HeaderText = "Did you want to save your changes?";
|
HeaderText = "Did you want to save your changes?";
|
||||||
|
|
||||||
@ -30,6 +30,7 @@ namespace osu.Game.Screens.Edit
|
|||||||
new PopupDialogCancelButton
|
new PopupDialogCancelButton
|
||||||
{
|
{
|
||||||
Text = @"Oops, continue editing",
|
Text = @"Oops, continue editing",
|
||||||
|
Action = cancel
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -103,7 +103,7 @@ namespace osu.Game.Screens.Menu
|
|||||||
OnEdit = delegate
|
OnEdit = delegate
|
||||||
{
|
{
|
||||||
Beatmap.SetDefault();
|
Beatmap.SetDefault();
|
||||||
this.Push(new Editor());
|
this.Push(new EditorLoader());
|
||||||
},
|
},
|
||||||
OnSolo = loadSoloSongSelect,
|
OnSolo = loadSoloSongSelect,
|
||||||
OnMultiplayer = () => this.Push(new Multiplayer()),
|
OnMultiplayer = () => this.Push(new Multiplayer()),
|
||||||
|
@ -32,6 +32,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private IAPIProvider api { get; set; }
|
private IAPIProvider api { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private ScoreManager scoreManager { get; set; }
|
||||||
|
|
||||||
public PlaylistsResultsScreen(ScoreInfo score, long roomId, PlaylistItem playlistItem, bool allowRetry, bool allowWatchingReplay = true)
|
public PlaylistsResultsScreen(ScoreInfo score, long roomId, PlaylistItem playlistItem, bool allowRetry, bool allowWatchingReplay = true)
|
||||||
: base(score, allowRetry, allowWatchingReplay)
|
: base(score, allowRetry, allowWatchingReplay)
|
||||||
{
|
{
|
||||||
@ -166,23 +169,28 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
|||||||
/// <param name="pivot">An optional pivot around which the scores were retrieved.</param>
|
/// <param name="pivot">An optional pivot around which the scores were retrieved.</param>
|
||||||
private void performSuccessCallback([NotNull] Action<IEnumerable<ScoreInfo>> callback, [NotNull] List<MultiplayerScore> scores, [CanBeNull] MultiplayerScores pivot = null)
|
private void performSuccessCallback([NotNull] Action<IEnumerable<ScoreInfo>> callback, [NotNull] List<MultiplayerScore> scores, [CanBeNull] MultiplayerScores pivot = null)
|
||||||
{
|
{
|
||||||
var scoreInfos = new List<ScoreInfo>(scores.Select(s => s.CreateScoreInfo(playlistItem)));
|
var scoreInfos = scores.Select(s => s.CreateScoreInfo(playlistItem)).ToArray();
|
||||||
|
|
||||||
// Select a score if we don't already have one selected.
|
// Score panels calculate total score before displaying, which can take some time. In order to count that calculation as part of the loading spinner display duration,
|
||||||
// Note: This is done before the callback so that the panel list centres on the selected score before panels are added (eliminating initial scroll).
|
// calculate the total scores locally before invoking the success callback.
|
||||||
if (SelectedScore.Value == null)
|
scoreManager.OrderByTotalScoreAsync(scoreInfos).ContinueWith(_ => Schedule(() =>
|
||||||
{
|
{
|
||||||
Schedule(() =>
|
// Select a score if we don't already have one selected.
|
||||||
|
// Note: This is done before the callback so that the panel list centres on the selected score before panels are added (eliminating initial scroll).
|
||||||
|
if (SelectedScore.Value == null)
|
||||||
{
|
{
|
||||||
// Prefer selecting the local user's score, or otherwise default to the first visible score.
|
Schedule(() =>
|
||||||
SelectedScore.Value = scoreInfos.FirstOrDefault(s => s.User.Id == api.LocalUser.Value.Id) ?? scoreInfos.FirstOrDefault();
|
{
|
||||||
});
|
// Prefer selecting the local user's score, or otherwise default to the first visible score.
|
||||||
}
|
SelectedScore.Value = scoreInfos.FirstOrDefault(s => s.User.Id == api.LocalUser.Value.Id) ?? scoreInfos.FirstOrDefault();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Invoke callback to add the scores. Exclude the user's current score which was added previously.
|
// Invoke callback to add the scores. Exclude the user's current score which was added previously.
|
||||||
callback.Invoke(scoreInfos.Where(s => s.OnlineScoreID != Score?.OnlineScoreID));
|
callback.Invoke(scoreInfos.Where(s => s.OnlineScoreID != Score?.OnlineScoreID));
|
||||||
|
|
||||||
hideLoadingSpinners(pivot);
|
hideLoadingSpinners(pivot);
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void hideLoadingSpinners([CanBeNull] MultiplayerScores pivot = null)
|
private void hideLoadingSpinners([CanBeNull] MultiplayerScores pivot = null)
|
||||||
|
@ -186,17 +186,14 @@ namespace osu.Game.Screens
|
|||||||
{
|
{
|
||||||
applyArrivingDefaults(false);
|
applyArrivingDefaults(false);
|
||||||
|
|
||||||
backgroundStack?.Push(ownedBackground = CreateBackground());
|
if (backgroundStack?.Push(ownedBackground = CreateBackground()) != true)
|
||||||
|
|
||||||
background = backgroundStack?.CurrentScreen as BackgroundScreen;
|
|
||||||
|
|
||||||
if (background != ownedBackground)
|
|
||||||
{
|
{
|
||||||
// background may have not been replaced, at which point we don't want to track the background lifetime.
|
// If the constructed instance was not actually pushed to the background stack, we don't want to track it unnecessarily.
|
||||||
ownedBackground?.Dispose();
|
ownedBackground?.Dispose();
|
||||||
ownedBackground = null;
|
ownedBackground = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
background = backgroundStack?.CurrentScreen as BackgroundScreen;
|
||||||
base.OnEntering(last);
|
base.OnEntering(last);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,8 +52,7 @@ namespace osu.Game.Screens.Ranking
|
|||||||
private Drawable bottomPanel;
|
private Drawable bottomPanel;
|
||||||
private Container<ScorePanel> detachedPanelContainer;
|
private Container<ScorePanel> detachedPanelContainer;
|
||||||
|
|
||||||
private bool fetchedInitialScores;
|
private bool lastFetchCompleted;
|
||||||
private APIRequest nextPageRequest;
|
|
||||||
|
|
||||||
private readonly bool allowRetry;
|
private readonly bool allowRetry;
|
||||||
private readonly bool allowWatchingReplay;
|
private readonly bool allowWatchingReplay;
|
||||||
@ -191,8 +190,10 @@ namespace osu.Game.Screens.Ranking
|
|||||||
{
|
{
|
||||||
base.Update();
|
base.Update();
|
||||||
|
|
||||||
if (fetchedInitialScores && nextPageRequest == null)
|
if (lastFetchCompleted)
|
||||||
{
|
{
|
||||||
|
APIRequest nextPageRequest = null;
|
||||||
|
|
||||||
if (ScorePanelList.IsScrolledToStart)
|
if (ScorePanelList.IsScrolledToStart)
|
||||||
nextPageRequest = FetchNextPage(-1, fetchScoresCallback);
|
nextPageRequest = FetchNextPage(-1, fetchScoresCallback);
|
||||||
else if (ScorePanelList.IsScrolledToEnd)
|
else if (ScorePanelList.IsScrolledToEnd)
|
||||||
@ -200,10 +201,7 @@ namespace osu.Game.Screens.Ranking
|
|||||||
|
|
||||||
if (nextPageRequest != null)
|
if (nextPageRequest != null)
|
||||||
{
|
{
|
||||||
// Scheduled after children to give the list a chance to update its scroll position and not potentially trigger a second request too early.
|
lastFetchCompleted = false;
|
||||||
nextPageRequest.Success += () => ScheduleAfterChildren(() => nextPageRequest = null);
|
|
||||||
nextPageRequest.Failure += _ => ScheduleAfterChildren(() => nextPageRequest = null);
|
|
||||||
|
|
||||||
api.Queue(nextPageRequest);
|
api.Queue(nextPageRequest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -229,7 +227,7 @@ namespace osu.Game.Screens.Ranking
|
|||||||
foreach (var s in scores)
|
foreach (var s in scores)
|
||||||
addScore(s);
|
addScore(s);
|
||||||
|
|
||||||
fetchedInitialScores = true;
|
lastFetchCompleted = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
public override void OnEntering(IScreen last)
|
public override void OnEntering(IScreen last)
|
||||||
|
@ -5,10 +5,14 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
@ -36,12 +40,14 @@ namespace osu.Game.Screens.Ranking
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether this <see cref="ScorePanelList"/> can be scrolled and is currently scrolled to the start.
|
/// Whether this <see cref="ScorePanelList"/> can be scrolled and is currently scrolled to the start.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsScrolledToStart => flow.Count > 0 && scroll.ScrollableExtent > 0 && scroll.Current <= scroll_endpoint_distance;
|
public bool IsScrolledToStart => flow.Count > 0 && AllPanelsVisible && scroll.ScrollableExtent > 0 && scroll.Current <= scroll_endpoint_distance;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether this <see cref="ScorePanelList"/> can be scrolled and is currently scrolled to the end.
|
/// Whether this <see cref="ScorePanelList"/> can be scrolled and is currently scrolled to the end.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsScrolledToEnd => flow.Count > 0 && scroll.ScrollableExtent > 0 && scroll.IsScrolledToEnd(scroll_endpoint_distance);
|
public bool IsScrolledToEnd => flow.Count > 0 && AllPanelsVisible && scroll.ScrollableExtent > 0 && scroll.IsScrolledToEnd(scroll_endpoint_distance);
|
||||||
|
|
||||||
|
public bool AllPanelsVisible => flow.All(p => p.IsPresent);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The current scroll position.
|
/// The current scroll position.
|
||||||
@ -60,6 +66,13 @@ namespace osu.Game.Screens.Ranking
|
|||||||
|
|
||||||
public readonly Bindable<ScoreInfo> SelectedScore = new Bindable<ScoreInfo>();
|
public readonly Bindable<ScoreInfo> SelectedScore = new Bindable<ScoreInfo>();
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private ScoreManager scoreManager { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private BeatmapDifficultyCache difficultyCache { get; set; }
|
||||||
|
|
||||||
|
private readonly CancellationTokenSource loadCancellationSource = new CancellationTokenSource();
|
||||||
private readonly Flow flow;
|
private readonly Flow flow;
|
||||||
private readonly Scroll scroll;
|
private readonly Scroll scroll;
|
||||||
private ScorePanel expandedPanel;
|
private ScorePanel expandedPanel;
|
||||||
@ -90,6 +103,9 @@ namespace osu.Game.Screens.Ranking
|
|||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
|
foreach (var d in flow)
|
||||||
|
displayScore(d);
|
||||||
|
|
||||||
SelectedScore.BindValueChanged(selectedScoreChanged, true);
|
SelectedScore.BindValueChanged(selectedScoreChanged, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,36 +130,56 @@ namespace osu.Game.Screens.Ranking
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
flow.Add(panel.CreateTrackingContainer().With(d =>
|
var trackingContainer = panel.CreateTrackingContainer().With(d =>
|
||||||
{
|
{
|
||||||
d.Anchor = Anchor.Centre;
|
d.Anchor = Anchor.Centre;
|
||||||
d.Origin = Anchor.Centre;
|
d.Origin = Anchor.Centre;
|
||||||
}));
|
d.Hide();
|
||||||
|
});
|
||||||
|
|
||||||
|
flow.Add(trackingContainer);
|
||||||
|
|
||||||
if (IsLoaded)
|
if (IsLoaded)
|
||||||
{
|
displayScore(trackingContainer);
|
||||||
if (SelectedScore.Value == score)
|
|
||||||
{
|
|
||||||
SelectedScore.TriggerChange();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// We want the scroll position to remain relative to the expanded panel. When a new panel is added after the expanded panel, nothing needs to be done.
|
|
||||||
// But when a panel is added before the expanded panel, we need to offset the scroll position by the width of the new panel.
|
|
||||||
if (expandedPanel != null && flow.GetPanelIndex(score) < flow.GetPanelIndex(expandedPanel.Score))
|
|
||||||
{
|
|
||||||
// A somewhat hacky property is used here because we need to:
|
|
||||||
// 1) Scroll after the scroll container's visible range is updated.
|
|
||||||
// 2) Scroll before the scroll container's scroll position is updated.
|
|
||||||
// Without this, we would have a 1-frame positioning error which looks very jarring.
|
|
||||||
scroll.InstantScrollTarget = (scroll.InstantScrollTarget ?? scroll.Target) + ScorePanel.CONTRACTED_WIDTH + panel_spacing;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return panel;
|
return panel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void displayScore(ScorePanelTrackingContainer trackingContainer)
|
||||||
|
{
|
||||||
|
if (!IsLoaded)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var score = trackingContainer.Panel.Score;
|
||||||
|
|
||||||
|
// Calculating score can take a while in extreme scenarios, so only display scores after the process completes.
|
||||||
|
scoreManager.GetTotalScoreAsync(score)
|
||||||
|
.ContinueWith(totalScore => Schedule(() =>
|
||||||
|
{
|
||||||
|
flow.SetLayoutPosition(trackingContainer, totalScore.Result);
|
||||||
|
|
||||||
|
trackingContainer.Show();
|
||||||
|
|
||||||
|
if (SelectedScore.Value == score)
|
||||||
|
{
|
||||||
|
SelectedScore.TriggerChange();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// We want the scroll position to remain relative to the expanded panel. When a new panel is added after the expanded panel, nothing needs to be done.
|
||||||
|
// But when a panel is added before the expanded panel, we need to offset the scroll position by the width of the new panel.
|
||||||
|
if (expandedPanel != null && flow.GetPanelIndex(score) < flow.GetPanelIndex(expandedPanel.Score))
|
||||||
|
{
|
||||||
|
// A somewhat hacky property is used here because we need to:
|
||||||
|
// 1) Scroll after the scroll container's visible range is updated.
|
||||||
|
// 2) Scroll before the scroll container's scroll position is updated.
|
||||||
|
// Without this, we would have a 1-frame positioning error which looks very jarring.
|
||||||
|
scroll.InstantScrollTarget = (scroll.InstantScrollTarget ?? scroll.Target) + ScorePanel.CONTRACTED_WIDTH + panel_spacing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}), TaskContinuationOptions.OnlyOnRanToCompletion);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Brings a <see cref="ScoreInfo"/> to the centre of the screen and expands it.
|
/// Brings a <see cref="ScoreInfo"/> to the centre of the screen and expands it.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -267,6 +303,9 @@ namespace osu.Game.Screens.Ranking
|
|||||||
|
|
||||||
protected override bool OnKeyDown(KeyDownEvent e)
|
protected override bool OnKeyDown(KeyDownEvent e)
|
||||||
{
|
{
|
||||||
|
if (expandedPanel == null)
|
||||||
|
return base.OnKeyDown(e);
|
||||||
|
|
||||||
var expandedPanelIndex = flow.GetPanelIndex(expandedPanel.Score);
|
var expandedPanelIndex = flow.GetPanelIndex(expandedPanel.Score);
|
||||||
|
|
||||||
switch (e.Key)
|
switch (e.Key)
|
||||||
@ -285,6 +324,12 @@ namespace osu.Game.Screens.Ranking
|
|||||||
return base.OnKeyDown(e);
|
return base.OnKeyDown(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool isDisposing)
|
||||||
|
{
|
||||||
|
base.Dispose(isDisposing);
|
||||||
|
loadCancellationSource?.Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
private class Flow : FillFlowContainer<ScorePanelTrackingContainer>
|
private class Flow : FillFlowContainer<ScorePanelTrackingContainer>
|
||||||
{
|
{
|
||||||
public override IEnumerable<Drawable> FlowingChildren => applySorting(AliveInternalChildren);
|
public override IEnumerable<Drawable> FlowingChildren => applySorting(AliveInternalChildren);
|
||||||
@ -292,24 +337,8 @@ namespace osu.Game.Screens.Ranking
|
|||||||
public int GetPanelIndex(ScoreInfo score) => applySorting(Children).TakeWhile(s => s.Panel.Score != score).Count();
|
public int GetPanelIndex(ScoreInfo score) => applySorting(Children).TakeWhile(s => s.Panel.Score != score).Count();
|
||||||
|
|
||||||
private IEnumerable<ScorePanelTrackingContainer> applySorting(IEnumerable<Drawable> drawables) => drawables.OfType<ScorePanelTrackingContainer>()
|
private IEnumerable<ScorePanelTrackingContainer> applySorting(IEnumerable<Drawable> drawables) => drawables.OfType<ScorePanelTrackingContainer>()
|
||||||
.OrderByDescending(s => s.Panel.Score.TotalScore)
|
.OrderByDescending(GetLayoutPosition)
|
||||||
.ThenBy(s => s.Panel.Score.OnlineScoreID);
|
.ThenBy(s => s.Panel.Score.OnlineScoreID);
|
||||||
|
|
||||||
protected override int Compare(Drawable x, Drawable y)
|
|
||||||
{
|
|
||||||
var tX = (ScorePanelTrackingContainer)x;
|
|
||||||
var tY = (ScorePanelTrackingContainer)y;
|
|
||||||
|
|
||||||
int result = tY.Panel.Score.TotalScore.CompareTo(tX.Panel.Score.TotalScore);
|
|
||||||
|
|
||||||
if (result != 0)
|
|
||||||
return result;
|
|
||||||
|
|
||||||
if (tX.Panel.Score.OnlineScoreID == null || tY.Panel.Score.OnlineScoreID == null)
|
|
||||||
return base.Compare(x, y);
|
|
||||||
|
|
||||||
return tX.Panel.Score.OnlineScoreID.Value.CompareTo(tY.Panel.Score.OnlineScoreID.Value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class Scroll : OsuScrollContainer
|
private class Scroll : OsuScrollContainer
|
||||||
|
@ -4,6 +4,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
@ -66,6 +68,9 @@ namespace osu.Game.Screens.Select.Leaderboards
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private ScoreManager scoreManager { get; set; }
|
private ScoreManager scoreManager { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private BeatmapDifficultyCache difficultyCache { get; set; }
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private IBindable<RulesetInfo> ruleset { get; set; }
|
private IBindable<RulesetInfo> ruleset { get; set; }
|
||||||
|
|
||||||
@ -120,8 +125,13 @@ namespace osu.Game.Screens.Select.Leaderboards
|
|||||||
|
|
||||||
protected override bool IsOnlineScope => Scope != BeatmapLeaderboardScope.Local;
|
protected override bool IsOnlineScope => Scope != BeatmapLeaderboardScope.Local;
|
||||||
|
|
||||||
|
private CancellationTokenSource loadCancellationSource;
|
||||||
|
|
||||||
protected override APIRequest FetchScores(Action<IEnumerable<ScoreInfo>> scoresCallback)
|
protected override APIRequest FetchScores(Action<IEnumerable<ScoreInfo>> scoresCallback)
|
||||||
{
|
{
|
||||||
|
loadCancellationSource?.Cancel();
|
||||||
|
loadCancellationSource = new CancellationTokenSource();
|
||||||
|
|
||||||
if (Beatmap == null)
|
if (Beatmap == null)
|
||||||
{
|
{
|
||||||
PlaceholderState = PlaceholderState.NoneSelected;
|
PlaceholderState = PlaceholderState.NoneSelected;
|
||||||
@ -146,8 +156,8 @@ namespace osu.Game.Screens.Select.Leaderboards
|
|||||||
scores = scores.Where(s => s.Mods.Any(m => selectedMods.Contains(m.Acronym)));
|
scores = scores.Where(s => s.Mods.Any(m => selectedMods.Contains(m.Acronym)));
|
||||||
}
|
}
|
||||||
|
|
||||||
Scores = scores.OrderByDescending(s => s.TotalScore).ToArray();
|
scoreManager.OrderByTotalScoreAsync(scores.ToArray(), loadCancellationSource.Token)
|
||||||
PlaceholderState = Scores.Any() ? PlaceholderState.Successful : PlaceholderState.NoScores;
|
.ContinueWith(ordered => scoresCallback?.Invoke(ordered.Result), TaskContinuationOptions.OnlyOnRanToCompletion);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -182,8 +192,15 @@ namespace osu.Game.Screens.Select.Leaderboards
|
|||||||
|
|
||||||
req.Success += r =>
|
req.Success += r =>
|
||||||
{
|
{
|
||||||
scoresCallback?.Invoke(r.Scores.Select(s => s.CreateScoreInfo(rulesets)));
|
scoreManager.OrderByTotalScoreAsync(r.Scores.Select(s => s.CreateScoreInfo(rulesets)).ToArray(), loadCancellationSource.Token)
|
||||||
TopScore = r.UserScore?.CreateScoreInfo(rulesets);
|
.ContinueWith(ordered => Schedule(() =>
|
||||||
|
{
|
||||||
|
if (loadCancellationSource.IsCancellationRequested)
|
||||||
|
return;
|
||||||
|
|
||||||
|
scoresCallback?.Invoke(ordered.Result);
|
||||||
|
TopScore = r.UserScore?.CreateScoreInfo(rulesets);
|
||||||
|
}), TaskContinuationOptions.OnlyOnRanToCompletion);
|
||||||
};
|
};
|
||||||
|
|
||||||
return req;
|
return req;
|
||||||
|
@ -349,7 +349,7 @@ namespace osu.Game.Screens.Select
|
|||||||
throw new InvalidOperationException($"Attempted to edit when {nameof(AllowEditing)} is disabled");
|
throw new InvalidOperationException($"Attempted to edit when {nameof(AllowEditing)} is disabled");
|
||||||
|
|
||||||
Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap ?? beatmapNoDebounce);
|
Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap ?? beatmapNoDebounce);
|
||||||
this.Push(new Editor());
|
this.Push(new EditorLoader());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -367,6 +367,11 @@ namespace osu.Game.Tests.Visual
|
|||||||
Add(runner = new TestSceneTestRunner.TestRunner());
|
Add(runner = new TestSceneTestRunner.TestRunner());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void InitialiseFonts()
|
||||||
|
{
|
||||||
|
// skip fonts load as it's not required for testing purposes.
|
||||||
|
}
|
||||||
|
|
||||||
public void RunTestBlocking(TestScene test) => runner.RunTestBlocking(test);
|
public void RunTestBlocking(TestScene test) => runner.RunTestBlocking(test);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,8 +36,8 @@
|
|||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Realm" Version="10.3.0" />
|
<PackageReference Include="Realm" Version="10.3.0" />
|
||||||
<PackageReference Include="ppy.osu.Framework" Version="2021.830.0" />
|
<PackageReference Include="ppy.osu.Framework" Version="2021.907.0" />
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.827.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.907.0" />
|
||||||
<PackageReference Include="Sentry" Version="3.9.0" />
|
<PackageReference Include="Sentry" Version="3.9.0" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.28.3" />
|
<PackageReference Include="SharpCompress" Version="0.28.3" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||||
|
@ -70,8 +70,8 @@
|
|||||||
<Reference Include="System.Net.Http" />
|
<Reference Include="System.Net.Http" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.830.0" />
|
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.907.0" />
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.827.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.907.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
|
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
@ -93,7 +93,7 @@
|
|||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||||
<PackageReference Include="ppy.osu.Framework" Version="2021.830.0" />
|
<PackageReference Include="ppy.osu.Framework" Version="2021.907.0" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.28.3" />
|
<PackageReference Include="SharpCompress" Version="0.28.3" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||||
<PackageReference Include="SharpRaven" Version="2.4.0" />
|
<PackageReference Include="SharpRaven" Version="2.4.0" />
|
||||||
|
Loading…
x
Reference in New Issue
Block a user