mirror of
https://github.com/ppy/osu.git
synced 2025-01-13 07:22:54 +08:00
Merge branch 'master' into fix-overzealousmouse-button-blocking
This commit is contained in:
commit
38e95a0e73
@ -52,6 +52,6 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.329.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.402.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
@ -18,6 +19,7 @@ using osu.Framework.Screens;
|
||||
using osu.Game.Screens.Menu;
|
||||
using osu.Game.Updater;
|
||||
using osu.Desktop.Windows;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.IO;
|
||||
|
||||
namespace osu.Desktop
|
||||
@ -144,13 +146,39 @@ namespace osu.Desktop
|
||||
desktopWindow.DragDrop += f => fileDrop(new[] { f });
|
||||
}
|
||||
|
||||
private readonly List<string> importableFiles = new List<string>();
|
||||
private ScheduledDelegate importSchedule;
|
||||
|
||||
private void fileDrop(string[] filePaths)
|
||||
{
|
||||
var firstExtension = Path.GetExtension(filePaths.First());
|
||||
lock (importableFiles)
|
||||
{
|
||||
var firstExtension = Path.GetExtension(filePaths.First());
|
||||
|
||||
if (filePaths.Any(f => Path.GetExtension(f) != firstExtension)) return;
|
||||
if (filePaths.Any(f => Path.GetExtension(f) != firstExtension)) return;
|
||||
|
||||
Task.Factory.StartNew(() => Import(filePaths), TaskCreationOptions.LongRunning);
|
||||
importableFiles.AddRange(filePaths);
|
||||
|
||||
Logger.Log($"Adding {filePaths.Length} files for import");
|
||||
|
||||
// File drag drop operations can potentially trigger hundreds or thousands of these calls on some platforms.
|
||||
// In order to avoid spawning multiple import tasks for a single drop operation, debounce a touch.
|
||||
importSchedule?.Cancel();
|
||||
importSchedule = Scheduler.AddDelayed(handlePendingImports, 100);
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePendingImports()
|
||||
{
|
||||
lock (importableFiles)
|
||||
{
|
||||
Logger.Log($"Handling batch import of {importableFiles.Count} files");
|
||||
|
||||
var paths = importableFiles.ToArray();
|
||||
importableFiles.Clear();
|
||||
|
||||
Task.Factory.StartNew(() => Import(paths), TaskCreationOptions.LongRunning);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
public void Test(double expected, string name)
|
||||
=> base.Test(expected, name);
|
||||
|
||||
[TestCase(5.0565038923984691d, "diffcalc-test")]
|
||||
[TestCase(5.169743871843191d, "diffcalc-test")]
|
||||
public void TestClockRateAdjusted(double expected, string name)
|
||||
=> Test(expected, name, new CatchModDoubleTime());
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
|
||||
<PackageReference Include="NUnit" Version="3.13.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
|
@ -3,13 +3,16 @@
|
||||
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
public class CatchModHidden : ModHidden
|
||||
public class CatchModHidden : ModHidden, IApplicableToDrawableRuleset<CatchHitObject>
|
||||
{
|
||||
public override string Description => @"Play with fading fruits.";
|
||||
public override double ScoreMultiplier => 1.06;
|
||||
@ -17,6 +20,14 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
private const double fade_out_offset_multiplier = 0.6;
|
||||
private const double fade_out_duration_multiplier = 0.44;
|
||||
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<CatchHitObject> drawableRuleset)
|
||||
{
|
||||
var drawableCatchRuleset = (DrawableCatchRuleset)drawableRuleset;
|
||||
var catchPlayfield = (CatchPlayfield)drawableCatchRuleset.Playfield;
|
||||
|
||||
catchPlayfield.CatcherArea.MovableCatcher.CatchFruitOnPlate = false;
|
||||
}
|
||||
|
||||
protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state)
|
||||
{
|
||||
base.ApplyNormalVisibilityState(hitObject, state);
|
||||
|
@ -43,6 +43,11 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
/// </summary>
|
||||
public bool HyperDashing => hyperDashModifier != 1;
|
||||
|
||||
/// <summary>
|
||||
/// Whether <see cref="DrawablePalpableCatchHitObject"/> fruit should appear on the plate.
|
||||
/// </summary>
|
||||
public bool CatchFruitOnPlate { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// The relative space to cover in 1 millisecond. based on 1 game pixel per millisecond as in osu-stable.
|
||||
/// </summary>
|
||||
@ -237,7 +242,8 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
{
|
||||
var positionInStack = computePositionInStack(new Vector2(palpableObject.X - X, 0), palpableObject.DisplaySize.X / 2);
|
||||
|
||||
placeCaughtObject(palpableObject, positionInStack);
|
||||
if (CatchFruitOnPlate)
|
||||
placeCaughtObject(palpableObject, positionInStack);
|
||||
|
||||
if (hitLighting.Value)
|
||||
addLighting(hitObject, positionInStack.X, drawableObject.AccentColour.Value);
|
||||
|
@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
public void Test(double expected, string name)
|
||||
=> base.Test(expected, name);
|
||||
|
||||
[TestCase(2.7646128945056723d, "diffcalc-test")]
|
||||
[TestCase(2.7879104989252959d, "diffcalc-test")]
|
||||
public void TestClockRateAdjusted(double expected, string name)
|
||||
=> Test(expected, name, new ManiaModDoubleTime());
|
||||
|
||||
|
@ -288,17 +288,56 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
.All(j => j.Type.IsHit()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHitTailBeforeLastTick()
|
||||
{
|
||||
const int tick_rate = 8;
|
||||
const double tick_spacing = TimingControlPoint.DEFAULT_BEAT_LENGTH / tick_rate;
|
||||
const double time_last_tick = time_head + tick_spacing * (int)((time_tail - time_head) / tick_spacing - 1);
|
||||
|
||||
var beatmap = new Beatmap<ManiaHitObject>
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new HoldNote
|
||||
{
|
||||
StartTime = time_head,
|
||||
Duration = time_tail - time_head,
|
||||
Column = 0,
|
||||
}
|
||||
},
|
||||
BeatmapInfo =
|
||||
{
|
||||
BaseDifficulty = new BeatmapDifficulty { SliderTickRate = tick_rate },
|
||||
Ruleset = new ManiaRuleset().RulesetInfo
|
||||
},
|
||||
};
|
||||
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(time_head, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(time_last_tick - 5)
|
||||
}, beatmap);
|
||||
|
||||
assertHeadJudgement(HitResult.Perfect);
|
||||
assertLastTickJudgement(HitResult.LargeTickMiss);
|
||||
assertTailJudgement(HitResult.Ok);
|
||||
}
|
||||
|
||||
private void assertHeadJudgement(HitResult result)
|
||||
=> AddAssert($"head judged as {result}", () => judgementResults[0].Type == result);
|
||||
=> AddAssert($"head judged as {result}", () => judgementResults.First(j => j.HitObject is Note).Type == result);
|
||||
|
||||
private void assertTailJudgement(HitResult result)
|
||||
=> AddAssert($"tail judged as {result}", () => judgementResults[^2].Type == result);
|
||||
=> AddAssert($"tail judged as {result}", () => judgementResults.Single(j => j.HitObject is TailNote).Type == result);
|
||||
|
||||
private void assertNoteJudgement(HitResult result)
|
||||
=> AddAssert($"hold note judged as {result}", () => judgementResults[^1].Type == result);
|
||||
=> AddAssert($"hold note judged as {result}", () => judgementResults.Single(j => j.HitObject is HoldNote).Type == result);
|
||||
|
||||
private void assertTickJudgement(HitResult result)
|
||||
=> AddAssert($"tick judged as {result}", () => judgementResults[6].Type == result); // arbitrary tick
|
||||
=> AddAssert($"any tick judged as {result}", () => judgementResults.Where(j => j.HitObject is HoldNoteTick).Any(j => j.Type == result));
|
||||
|
||||
private void assertLastTickJudgement(HitResult result)
|
||||
=> AddAssert($"last tick judged as {result}", () => judgementResults.Last(j => j.HitObject is HoldNoteTick).Type == result);
|
||||
|
||||
private ScoreAccessibleReplayPlayer currentPlayer;
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
|
||||
<PackageReference Include="NUnit" Version="3.13.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
|
@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
|
||||
if (IsForCurrentRuleset)
|
||||
{
|
||||
TargetColumns = (int)Math.Max(1, roundedCircleSize);
|
||||
TargetColumns = GetColumnCountForNonConvert(beatmap.BeatmapInfo);
|
||||
|
||||
if (TargetColumns > ManiaRuleset.MAX_STAGE_KEYS)
|
||||
{
|
||||
@ -71,6 +71,12 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
originalTargetColumns = TargetColumns;
|
||||
}
|
||||
|
||||
public static int GetColumnCountForNonConvert(BeatmapInfo beatmap)
|
||||
{
|
||||
var roundedCircleSize = Math.Round(beatmap.BaseDifficulty.CircleSize);
|
||||
return (int)Math.Max(1, roundedCircleSize);
|
||||
}
|
||||
|
||||
public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition);
|
||||
|
||||
protected override Beatmap<ManiaHitObject> ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken)
|
||||
|
@ -73,8 +73,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
|
||||
}
|
||||
|
||||
protected override double GetPeakStrain(double offset)
|
||||
=> applyDecay(individualStrain, offset - Previous[0].BaseObject.StartTime, individual_decay_base)
|
||||
+ applyDecay(overallStrain, offset - Previous[0].BaseObject.StartTime, overall_decay_base);
|
||||
=> applyDecay(individualStrain, offset - Previous[0].StartTime, individual_decay_base)
|
||||
+ applyDecay(overallStrain, offset - Previous[0].StartTime, overall_decay_base);
|
||||
|
||||
private double applyDecay(double value, double deltaTime, double decayBase)
|
||||
=> value * Math.Pow(decayBase, deltaTime / 1000);
|
||||
|
33
osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs
Normal file
33
osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs
Normal file
@ -0,0 +1,33 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Filter;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania
|
||||
{
|
||||
public class ManiaFilterCriteria : IRulesetFilterCriteria
|
||||
{
|
||||
private FilterCriteria.OptionalRange<float> keys;
|
||||
|
||||
public bool Matches(BeatmapInfo beatmap)
|
||||
{
|
||||
return !keys.HasFilter || (beatmap.RulesetID == new ManiaRuleset().LegacyID && keys.IsInRange(ManiaBeatmapConverter.GetColumnCountForNonConvert(beatmap)));
|
||||
}
|
||||
|
||||
public bool TryParseCustomKeywordCriteria(string key, Operator op, string value)
|
||||
{
|
||||
switch (key)
|
||||
{
|
||||
case "key":
|
||||
case "keys":
|
||||
return FilterQueryParser.TryUpdateCriteriaRange(ref keys, op, value);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
@ -22,6 +22,7 @@ using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Rulesets.Configuration;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Filter;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Configuration;
|
||||
using osu.Game.Rulesets.Mania.Difficulty;
|
||||
@ -382,6 +383,11 @@ namespace osu.Game.Rulesets.Mania
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public override IRulesetFilterCriteria CreateRulesetFilterCriteria()
|
||||
{
|
||||
return new ManiaFilterCriteria();
|
||||
}
|
||||
}
|
||||
|
||||
public enum PlayfieldType
|
||||
|
@ -233,6 +233,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
{
|
||||
if (Tail.AllJudged)
|
||||
{
|
||||
foreach (var tick in tickContainer)
|
||||
{
|
||||
if (!tick.Judged)
|
||||
tick.MissForcefully();
|
||||
}
|
||||
|
||||
ApplyResult(r => r.Type = r.Judgement.MaxResult);
|
||||
endHold();
|
||||
}
|
||||
|
@ -20,8 +20,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
public void Test(double expected, string name)
|
||||
=> base.Test(expected, name);
|
||||
|
||||
[TestCase(8.6228371119271454d, "diffcalc-test")]
|
||||
[TestCase(1.2864585280364178d, "zero-length-sliders")]
|
||||
[TestCase(8.7212283220412345d, "diffcalc-test")]
|
||||
[TestCase(1.3212137158641493d, "zero-length-sliders")]
|
||||
public void TestClockRateAdjusted(double expected, string name)
|
||||
=> Test(expected, name, new OsuModDoubleTime());
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
|
||||
<PackageReference Include="NUnit" Version="3.13.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
|
@ -44,6 +44,9 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
[SettingSource("Use fixed slider follow circle hit area", "Makes the slider follow circle track its final size at all times.")]
|
||||
public Bindable<bool> FixedFollowCircleHitArea { get; } = new BindableBool(true);
|
||||
|
||||
[SettingSource("Always play a slider's tail sample", "Always plays a slider's tail sample regardless of whether it was hit or not.")]
|
||||
public Bindable<bool> AlwaysPlayTailSample { get; } = new BindableBool(true);
|
||||
|
||||
public void ApplyToHitObject(HitObject hitObject)
|
||||
{
|
||||
switch (hitObject)
|
||||
@ -79,6 +82,10 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
case DrawableSliderHead head:
|
||||
head.TrackFollowCircle = !NoSliderHeadMovement.Value;
|
||||
break;
|
||||
|
||||
case DrawableSliderTail tail:
|
||||
tail.SamplePlaysOnlyOnHit = !AlwaysPlayTailSample.Value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -280,7 +280,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
// rather than doing it this way, we should probably attach the sample to the tail circle.
|
||||
// this can only be done after we stop using LegacyLastTick.
|
||||
if (TailCircle.IsHit)
|
||||
if (!TailCircle.SamplePlaysOnlyOnHit || TailCircle.IsHit)
|
||||
base.PlaySamples();
|
||||
}
|
||||
|
||||
|
@ -26,6 +26,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
/// </summary>
|
||||
public override bool DisplayResult => false;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the hit samples only play on successful hits.
|
||||
/// If <c>false</c>, the hit samples will also play on misses.
|
||||
/// </summary>
|
||||
public bool SamplePlaysOnlyOnHit { get; set; } = true;
|
||||
|
||||
public bool Tracking { get; set; }
|
||||
|
||||
private SkinnableDrawable circlePiece;
|
||||
|
@ -19,8 +19,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
public void Test(double expected, string name)
|
||||
=> base.Test(expected, name);
|
||||
|
||||
[TestCase(3.1473940254109078d, "diffcalc-test")]
|
||||
[TestCase(3.1473940254109078d, "diffcalc-test-strong")]
|
||||
[TestCase(3.1704781712282624d, "diffcalc-test")]
|
||||
[TestCase(3.1704781712282624d, "diffcalc-test-strong")]
|
||||
public void TestClockRateAdjusted(double expected, string name)
|
||||
=> Test(expected, name, new TaikoModDoubleTime());
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
|
||||
<PackageReference Include="NUnit" Version="3.13.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
|
@ -3,6 +3,8 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
@ -11,6 +13,7 @@ using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Online;
|
||||
using osu.Game.Online.Spectator;
|
||||
using osu.Game.Replays.Legacy;
|
||||
@ -29,10 +32,13 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Cached(typeof(SpectatorStreamingClient))]
|
||||
private TestSpectatorStreamingClient testSpectatorStreamingClient = new TestSpectatorStreamingClient();
|
||||
|
||||
[Cached(typeof(UserLookupCache))]
|
||||
private UserLookupCache lookupCache = new TestUserLookupCache();
|
||||
|
||||
// used just to show beatmap card for the time being.
|
||||
protected override bool UseOnlineAPI => true;
|
||||
|
||||
private Spectator spectatorScreen;
|
||||
private SoloSpectator spectatorScreen;
|
||||
|
||||
[Resolved]
|
||||
private OsuGameBase game { get; set; }
|
||||
@ -69,7 +75,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
loadSpectatingScreen();
|
||||
|
||||
AddAssert("screen hasn't changed", () => Stack.CurrentScreen is Spectator);
|
||||
AddAssert("screen hasn't changed", () => Stack.CurrentScreen is SoloSpectator);
|
||||
|
||||
start();
|
||||
sendFrames();
|
||||
@ -195,7 +201,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
start(-1234);
|
||||
sendFrames();
|
||||
|
||||
AddAssert("screen didn't change", () => Stack.CurrentScreen is Spectator);
|
||||
AddAssert("screen didn't change", () => Stack.CurrentScreen is SoloSpectator);
|
||||
}
|
||||
|
||||
private OsuFramedReplayInputHandler replayHandler =>
|
||||
@ -226,7 +232,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
private void loadSpectatingScreen()
|
||||
{
|
||||
AddStep("load screen", () => LoadScreen(spectatorScreen = new Spectator(testSpectatorStreamingClient.StreamingUser)));
|
||||
AddStep("load screen", () => LoadScreen(spectatorScreen = new SoloSpectator(testSpectatorStreamingClient.StreamingUser)));
|
||||
AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded);
|
||||
}
|
||||
|
||||
@ -301,5 +307,14 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
internal class TestUserLookupCache : UserLookupCache
|
||||
{
|
||||
protected override Task<User> ComputeValueAsync(int lookup, CancellationToken token = default) => Task.FromResult(new User
|
||||
{
|
||||
Id = lookup,
|
||||
Username = $"User {lookup}"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -43,6 +43,29 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
exitViaEscapeAndConfirm();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRetryCountIncrements()
|
||||
{
|
||||
Player player = null;
|
||||
|
||||
PushAndConfirm(() => new TestSongSelect());
|
||||
|
||||
AddStep("import beatmap", () => ImportBeatmapTest.LoadQuickOszIntoOsu(Game).Wait());
|
||||
|
||||
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
|
||||
|
||||
AddStep("press enter", () => InputManager.Key(Key.Enter));
|
||||
|
||||
AddUntilStep("wait for player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null);
|
||||
AddAssert("retry count is 0", () => player.RestartCount == 0);
|
||||
|
||||
AddStep("attempt to retry", () => player.ChildrenOfType<HotkeyRetryOverlay>().First().Action());
|
||||
AddUntilStep("wait for old player gone", () => Game.ScreenStack.CurrentScreen != player);
|
||||
|
||||
AddUntilStep("get new player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null);
|
||||
AddAssert("retry count is 1", () => player.RestartCount == 1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRetryFromResults()
|
||||
{
|
||||
|
@ -45,6 +45,8 @@ namespace osu.Game.Tests.Visual.Settings
|
||||
public Bindable<Vector2> AreaOffset { get; } = new Bindable<Vector2>();
|
||||
public Bindable<Vector2> AreaSize { get; } = new Bindable<Vector2>();
|
||||
|
||||
public Bindable<float> Rotation { get; } = new Bindable<float>();
|
||||
|
||||
public IBindable<TabletInfo> Tablet => tablet;
|
||||
|
||||
private readonly Bindable<TabletInfo> tablet = new Bindable<TabletInfo>();
|
||||
|
@ -3,7 +3,7 @@
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
||||
<PackageReference Include="DeepEqual" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
|
||||
<PackageReference Include="NUnit" Version="3.13.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
|
@ -5,7 +5,7 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
|
||||
<PackageReference Include="NUnit" Version="3.13.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
|
||||
</ItemGroup>
|
||||
|
@ -147,7 +147,7 @@ namespace osu.Game.Beatmaps
|
||||
{
|
||||
string version = string.IsNullOrEmpty(Version) ? string.Empty : $"[{Version}]";
|
||||
|
||||
return $"{Metadata} {version}".Trim();
|
||||
return $"{Metadata ?? BeatmapSet?.Metadata} {version}".Trim();
|
||||
}
|
||||
|
||||
public bool Equals(BeatmapInfo other)
|
||||
|
@ -19,8 +19,13 @@ namespace osu.Game.Beatmaps
|
||||
public int ID { get; set; }
|
||||
|
||||
public string Title { get; set; }
|
||||
|
||||
[JsonProperty("title_unicode")]
|
||||
public string TitleUnicode { get; set; }
|
||||
|
||||
public string Artist { get; set; }
|
||||
|
||||
[JsonProperty("artist_unicode")]
|
||||
public string ArtistUnicode { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
|
@ -61,6 +61,9 @@ namespace osu.Game.Online.Leaderboards
|
||||
[Resolved(CanBeNull = true)]
|
||||
private SongSelect songSelect { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private ScoreManager scoreManager { get; set; }
|
||||
|
||||
public LeaderboardScore(ScoreInfo score, int? rank, bool allowHighlight = true)
|
||||
{
|
||||
this.score = score;
|
||||
@ -388,6 +391,9 @@ namespace osu.Game.Online.Leaderboards
|
||||
if (score.Mods.Length > 0 && modsContainer.Any(s => s.IsHovered) && songSelect != null)
|
||||
items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = score.Mods));
|
||||
|
||||
if (score.Files.Count > 0)
|
||||
items.Add(new OsuMenuItem("Export", MenuItemType.Standard, () => scoreManager.Export(score)));
|
||||
|
||||
if (score.ID != 0)
|
||||
items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(score))));
|
||||
|
||||
|
@ -429,6 +429,9 @@ namespace osu.Game
|
||||
|
||||
public async Task Import(params string[] paths)
|
||||
{
|
||||
if (paths.Length == 0)
|
||||
return;
|
||||
|
||||
var extension = Path.GetExtension(paths.First())?.ToLowerInvariant();
|
||||
|
||||
foreach (var importer in fileImporters)
|
||||
|
@ -8,6 +8,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps.Drawables;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
@ -228,8 +229,8 @@ namespace osu.Game.Overlays.BeatmapSet
|
||||
|
||||
loading.Hide();
|
||||
|
||||
title.Text = setInfo.NewValue.Metadata.Title ?? string.Empty;
|
||||
artist.Text = setInfo.NewValue.Metadata.Artist ?? string.Empty;
|
||||
title.Text = new RomanisableString(setInfo.NewValue.Metadata.TitleUnicode, setInfo.NewValue.Metadata.Title);
|
||||
artist.Text = new RomanisableString(setInfo.NewValue.Metadata.ArtistUnicode, setInfo.NewValue.Metadata.Artist);
|
||||
|
||||
explicitContentPill.Alpha = setInfo.NewValue.OnlineInfo.HasExplicitContent ? 1 : 0;
|
||||
|
||||
|
@ -137,7 +137,7 @@ namespace osu.Game.Overlays.Dashboard
|
||||
Text = "Watch",
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Action = () => game?.PerformFromScreen(s => s.Push(new Spectator(User))),
|
||||
Action = () => game?.PerformFromScreen(s => s.Push(new SoloSpectator(User))),
|
||||
Enabled = { Value = User.Id != api.LocalUser.Value.Id }
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,109 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input.Handlers.Tablet;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
|
||||
namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
{
|
||||
internal class RotationPresetButtons : FillFlowContainer
|
||||
{
|
||||
private readonly ITabletHandler tabletHandler;
|
||||
|
||||
private Bindable<float> rotation;
|
||||
|
||||
private const int height = 50;
|
||||
|
||||
public RotationPresetButtons(ITabletHandler tabletHandler)
|
||||
{
|
||||
this.tabletHandler = tabletHandler;
|
||||
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Height = height;
|
||||
|
||||
for (int i = 0; i < 360; i += 90)
|
||||
{
|
||||
var presetRotation = i;
|
||||
|
||||
Add(new RotationButton(i)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = height,
|
||||
Width = 0.25f,
|
||||
Text = $"{presetRotation}º",
|
||||
Action = () => tabletHandler.Rotation.Value = presetRotation,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
rotation = tabletHandler.Rotation.GetBoundCopy();
|
||||
rotation.BindValueChanged(val =>
|
||||
{
|
||||
foreach (var b in Children.OfType<RotationButton>())
|
||||
b.IsSelected = b.Preset == val.NewValue;
|
||||
}, true);
|
||||
}
|
||||
|
||||
public class RotationButton : TriangleButton
|
||||
{
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; }
|
||||
|
||||
public readonly int Preset;
|
||||
|
||||
public RotationButton(int preset)
|
||||
{
|
||||
Preset = preset;
|
||||
}
|
||||
|
||||
private bool isSelected;
|
||||
|
||||
public bool IsSelected
|
||||
{
|
||||
get => isSelected;
|
||||
set
|
||||
{
|
||||
if (value == isSelected)
|
||||
return;
|
||||
|
||||
isSelected = value;
|
||||
|
||||
if (IsLoaded)
|
||||
updateColour();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
updateColour();
|
||||
}
|
||||
|
||||
private void updateColour()
|
||||
{
|
||||
if (isSelected)
|
||||
{
|
||||
BackgroundColour = colours.BlueDark;
|
||||
Triangles.ColourDark = colours.BlueDarker;
|
||||
Triangles.ColourLight = colours.Blue;
|
||||
}
|
||||
else
|
||||
{
|
||||
BackgroundColour = colours.Gray4;
|
||||
Triangles.ColourDark = colours.Gray5;
|
||||
Triangles.ColourLight = colours.Gray6;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -25,6 +25,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
private readonly Bindable<Vector2> areaOffset = new Bindable<Vector2>();
|
||||
private readonly Bindable<Vector2> areaSize = new Bindable<Vector2>();
|
||||
|
||||
private readonly BindableNumber<float> rotation = new BindableNumber<float>();
|
||||
|
||||
private readonly IBindable<TabletInfo> tablet = new Bindable<TabletInfo>();
|
||||
|
||||
private OsuSpriteText tabletName;
|
||||
@ -124,6 +126,13 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
usableAreaText.Text = $"{(float)x / commonDivider}:{(float)y / commonDivider}";
|
||||
}, true);
|
||||
|
||||
rotation.BindTo(handler.Rotation);
|
||||
rotation.BindValueChanged(val =>
|
||||
{
|
||||
usableAreaContainer.RotateTo(val.NewValue, 100, Easing.OutQuint)
|
||||
.OnComplete(_ => checkBounds()); // required as we are using SSDQ.
|
||||
});
|
||||
|
||||
tablet.BindTo(handler.Tablet);
|
||||
tablet.BindValueChanged(_ => Scheduler.AddOnce(updateTabletDetails));
|
||||
|
||||
|
@ -27,6 +27,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
private readonly BindableNumber<float> sizeX = new BindableNumber<float> { MinValue = 10 };
|
||||
private readonly BindableNumber<float> sizeY = new BindableNumber<float> { MinValue = 10 };
|
||||
|
||||
private readonly BindableNumber<float> rotation = new BindableNumber<float> { MinValue = 0, MaxValue = 360 };
|
||||
|
||||
[Resolved]
|
||||
private GameHost host { get; set; }
|
||||
|
||||
@ -110,12 +112,6 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
}
|
||||
},
|
||||
new SettingsSlider<float>
|
||||
{
|
||||
TransferValueOnCommit = true,
|
||||
LabelText = "Aspect Ratio",
|
||||
Current = aspectRatio
|
||||
},
|
||||
new SettingsSlider<float>
|
||||
{
|
||||
TransferValueOnCommit = true,
|
||||
LabelText = "X Offset",
|
||||
@ -127,6 +123,19 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
LabelText = "Y Offset",
|
||||
Current = offsetY
|
||||
},
|
||||
new SettingsSlider<float>
|
||||
{
|
||||
TransferValueOnCommit = true,
|
||||
LabelText = "Rotation",
|
||||
Current = rotation
|
||||
},
|
||||
new RotationPresetButtons(tabletHandler),
|
||||
new SettingsSlider<float>
|
||||
{
|
||||
TransferValueOnCommit = true,
|
||||
LabelText = "Aspect Ratio",
|
||||
Current = aspectRatio
|
||||
},
|
||||
new SettingsCheckbox
|
||||
{
|
||||
LabelText = "Lock aspect ratio",
|
||||
@ -153,6 +162,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
rotation.BindTo(tabletHandler.Rotation);
|
||||
|
||||
areaOffset.BindTo(tabletHandler.AreaOffset);
|
||||
areaOffset.BindValueChanged(val =>
|
||||
{
|
||||
|
@ -83,7 +83,7 @@ namespace osu.Game.Rulesets.Difficulty
|
||||
foreach (Skill s in skills)
|
||||
{
|
||||
s.SaveCurrentPeak();
|
||||
s.StartNewSectionFrom(currentSectionEnd);
|
||||
s.StartNewSectionFrom(currentSectionEnd / clockRate);
|
||||
}
|
||||
|
||||
currentSectionEnd += sectionLength;
|
||||
|
@ -25,6 +25,16 @@ namespace osu.Game.Rulesets.Difficulty.Preprocessing
|
||||
/// </summary>
|
||||
public readonly double DeltaTime;
|
||||
|
||||
/// <summary>
|
||||
/// Clockrate adjusted start time of <see cref="BaseObject"/>.
|
||||
/// </summary>
|
||||
public readonly double StartTime;
|
||||
|
||||
/// <summary>
|
||||
/// Clockrate adjusted end time of <see cref="BaseObject"/>.
|
||||
/// </summary>
|
||||
public readonly double EndTime;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="DifficultyHitObject"/>.
|
||||
/// </summary>
|
||||
@ -36,6 +46,8 @@ namespace osu.Game.Rulesets.Difficulty.Preprocessing
|
||||
BaseObject = hitObject;
|
||||
LastObject = lastObject;
|
||||
DeltaTime = (hitObject.StartTime - lastObject.StartTime) / clockRate;
|
||||
StartTime = hitObject.StartTime / clockRate;
|
||||
EndTime = hitObject.GetEndTime() / clockRate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -88,7 +88,7 @@ namespace osu.Game.Rulesets.Difficulty.Skills
|
||||
/// <summary>
|
||||
/// Sets the initial strain level for a new section.
|
||||
/// </summary>
|
||||
/// <param name="time">The beginning of the new section in milliseconds.</param>
|
||||
/// <param name="time">The beginning of the new section in milliseconds, adjusted by clockrate.</param>
|
||||
public void StartNewSectionFrom(double time)
|
||||
{
|
||||
// The maximum strain of the new section is not zero by default, strain decays as usual regardless of section boundaries.
|
||||
@ -100,9 +100,9 @@ namespace osu.Game.Rulesets.Difficulty.Skills
|
||||
/// <summary>
|
||||
/// Retrieves the peak strain at a point in time.
|
||||
/// </summary>
|
||||
/// <param name="time">The time to retrieve the peak strain at.</param>
|
||||
/// <param name="time">The time to retrieve the peak strain at, adjusted by clockrate.</param>
|
||||
/// <returns>The peak strain.</returns>
|
||||
protected virtual double GetPeakStrain(double time) => CurrentStrain * strainDecay(time - Previous[0].BaseObject.StartTime);
|
||||
protected virtual double GetPeakStrain(double time) => CurrentStrain * strainDecay(time - Previous[0].StartTime);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the calculated difficulty value representing all processed <see cref="DifficultyHitObject"/>s.
|
||||
|
@ -173,7 +173,7 @@ namespace osu.Game.Rulesets
|
||||
{
|
||||
var filename = Path.GetFileNameWithoutExtension(file);
|
||||
|
||||
if (loadedAssemblies.Values.Any(t => t.Namespace == filename))
|
||||
if (loadedAssemblies.Values.Any(t => Path.GetFileNameWithoutExtension(t.Assembly.Location) == filename))
|
||||
return;
|
||||
|
||||
try
|
||||
|
@ -54,7 +54,7 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
{
|
||||
FileSelector fileSelector;
|
||||
|
||||
Target.Child = fileSelector = new FileSelector(validFileExtensions: ResourcesSection.AudioExtensions)
|
||||
Target.Child = fileSelector = new FileSelector(currentFile.Value?.DirectoryName, ResourcesSection.AudioExtensions)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 400,
|
||||
|
@ -73,7 +73,8 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
audioTrackTextBox = new FileChooserLabelledTextBox
|
||||
{
|
||||
Label = "Audio Track",
|
||||
Current = { Value = working.Value.Metadata.AudioFile ?? "Click to select a track" },
|
||||
PlaceholderText = "Click to select a track",
|
||||
Current = { Value = working.Value.Metadata.AudioFile },
|
||||
Target = audioTrackFileChooserContainer,
|
||||
TabbableContentContainer = this
|
||||
},
|
||||
|
@ -28,7 +28,16 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
{
|
||||
if (point.NewValue != null)
|
||||
{
|
||||
multiplierSlider.Current = point.NewValue.SpeedMultiplierBindable;
|
||||
var selectedPointBindable = point.NewValue.SpeedMultiplierBindable;
|
||||
|
||||
// there may be legacy control points, which contain infinite precision for compatibility reasons (see LegacyDifficultyControlPoint).
|
||||
// generally that level of precision could only be set by externally editing the .osu file, so at the point
|
||||
// a user is looking to update this within the editor it should be safe to obliterate this additional precision.
|
||||
double expectedPrecision = new DifficultyControlPoint().SpeedMultiplierBindable.Precision;
|
||||
if (selectedPointBindable.Precision < expectedPrecision)
|
||||
selectedPointBindable.Precision = expectedPrecision;
|
||||
|
||||
multiplierSlider.Current = selectedPointBindable;
|
||||
multiplierSlider.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
|
||||
}
|
||||
}
|
||||
|
@ -309,10 +309,8 @@ namespace osu.Game.Screens.Play
|
||||
if (!this.IsCurrentScreen())
|
||||
return;
|
||||
|
||||
var restartCount = player?.RestartCount + 1 ?? 0;
|
||||
|
||||
player = createPlayer();
|
||||
player.RestartCount = restartCount;
|
||||
player.RestartCount = restartCount++;
|
||||
player.RestartRequested = restartRequested;
|
||||
|
||||
LoadTask = LoadComponentAsync(player, _ => MetadataInfo.Loading = false);
|
||||
@ -428,6 +426,8 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
private Bindable<bool> muteWarningShownOnce;
|
||||
|
||||
private int restartCount;
|
||||
|
||||
private void showMuteWarningIfNeeded()
|
||||
{
|
||||
if (!muteWarningShownOnce.Value)
|
||||
|
@ -1,18 +1,15 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
@ -24,73 +21,49 @@ using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.Spectator;
|
||||
using osu.Game.Overlays.BeatmapListing.Panels;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Rulesets.Replays.Types;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.OnlinePlay.Match.Components;
|
||||
using osu.Game.Screens.Spectate;
|
||||
using osu.Game.Users;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Play
|
||||
{
|
||||
[Cached(typeof(IPreviewTrackOwner))]
|
||||
public class Spectator : OsuScreen, IPreviewTrackOwner
|
||||
public class SoloSpectator : SpectatorScreen, IPreviewTrackOwner
|
||||
{
|
||||
[NotNull]
|
||||
private readonly User targetUser;
|
||||
|
||||
[Resolved]
|
||||
private Bindable<WorkingBeatmap> beatmap { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private Bindable<RulesetInfo> ruleset { get; set; }
|
||||
|
||||
private Ruleset rulesetInstance;
|
||||
|
||||
[Resolved]
|
||||
private Bindable<IReadOnlyList<Mod>> mods { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private SpectatorStreamingClient spectatorStreaming { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private BeatmapManager beatmaps { get; set; }
|
||||
private PreviewTrackManager previewTrackManager { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private PreviewTrackManager previewTrackManager { get; set; }
|
||||
|
||||
private Score score;
|
||||
|
||||
private readonly object scoreLock = new object();
|
||||
private BeatmapManager beatmaps { get; set; }
|
||||
|
||||
private Container beatmapPanelContainer;
|
||||
|
||||
private SpectatorState state;
|
||||
|
||||
private IBindable<WeakReference<BeatmapSetInfo>> managerUpdated;
|
||||
|
||||
private TriangleButton watchButton;
|
||||
|
||||
private SettingsCheckbox automaticDownload;
|
||||
|
||||
private BeatmapSetInfo onlineBeatmap;
|
||||
|
||||
/// <summary>
|
||||
/// Becomes true if a new state is waiting to be loaded (while this screen was not active).
|
||||
/// The player's immediate online gameplay state.
|
||||
/// This doesn't always reflect the gameplay state being watched.
|
||||
/// </summary>
|
||||
private bool newStatePending;
|
||||
private GameplayState immediateGameplayState;
|
||||
|
||||
public Spectator([NotNull] User targetUser)
|
||||
private GetBeatmapSetRequest onlineBeatmapRequest;
|
||||
|
||||
public SoloSpectator([NotNull] User targetUser)
|
||||
: base(targetUser.Id)
|
||||
{
|
||||
this.targetUser = targetUser ?? throw new ArgumentNullException(nameof(targetUser));
|
||||
this.targetUser = targetUser;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@ -173,7 +146,7 @@ namespace osu.Game.Screens.Play
|
||||
Width = 250,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Action = attemptStart,
|
||||
Action = () => scheduleStart(immediateGameplayState),
|
||||
Enabled = { Value = false }
|
||||
}
|
||||
}
|
||||
@ -185,169 +158,76 @@ namespace osu.Game.Screens.Play
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
spectatorStreaming.OnUserBeganPlaying += userBeganPlaying;
|
||||
spectatorStreaming.OnUserFinishedPlaying += userFinishedPlaying;
|
||||
spectatorStreaming.OnNewFrames += userSentFrames;
|
||||
|
||||
spectatorStreaming.WatchUser(targetUser.Id);
|
||||
|
||||
managerUpdated = beatmaps.ItemUpdated.GetBoundCopy();
|
||||
managerUpdated.BindValueChanged(beatmapUpdated);
|
||||
|
||||
automaticDownload.Current.BindValueChanged(_ => checkForAutomaticDownload());
|
||||
}
|
||||
|
||||
private void beatmapUpdated(ValueChangedEvent<WeakReference<BeatmapSetInfo>> beatmap)
|
||||
protected override void OnUserStateChanged(int userId, SpectatorState spectatorState)
|
||||
{
|
||||
if (beatmap.NewValue.TryGetTarget(out var beatmapSet) && beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID == state.BeatmapID))
|
||||
Schedule(attemptStart);
|
||||
clearDisplay();
|
||||
showBeatmapPanel(spectatorState);
|
||||
}
|
||||
|
||||
private void userSentFrames(int userId, FrameDataBundle data)
|
||||
protected override void StartGameplay(int userId, GameplayState gameplayState)
|
||||
{
|
||||
// this is not scheduled as it handles propagation of frames even when in a child screen (at which point we are not alive).
|
||||
// probably not the safest way to handle this.
|
||||
immediateGameplayState = gameplayState;
|
||||
watchButton.Enabled.Value = true;
|
||||
|
||||
if (userId != targetUser.Id)
|
||||
return;
|
||||
|
||||
lock (scoreLock)
|
||||
{
|
||||
// this should never happen as the server sends the user's state on watching,
|
||||
// but is here as a safety measure.
|
||||
if (score == null)
|
||||
return;
|
||||
|
||||
// rulesetInstance should be guaranteed to be in sync with the score via scoreLock.
|
||||
Debug.Assert(rulesetInstance != null && rulesetInstance.RulesetInfo.Equals(score.ScoreInfo.Ruleset));
|
||||
|
||||
foreach (var frame in data.Frames)
|
||||
{
|
||||
IConvertibleReplayFrame convertibleFrame = rulesetInstance.CreateConvertibleReplayFrame();
|
||||
convertibleFrame.FromLegacy(frame, beatmap.Value.Beatmap);
|
||||
|
||||
var convertedFrame = (ReplayFrame)convertibleFrame;
|
||||
convertedFrame.Time = frame.Time;
|
||||
|
||||
score.Replay.Frames.Add(convertedFrame);
|
||||
}
|
||||
}
|
||||
scheduleStart(gameplayState);
|
||||
}
|
||||
|
||||
private void userBeganPlaying(int userId, SpectatorState state)
|
||||
protected override void EndGameplay(int userId)
|
||||
{
|
||||
if (userId != targetUser.Id)
|
||||
return;
|
||||
scheduledStart?.Cancel();
|
||||
immediateGameplayState = null;
|
||||
watchButton.Enabled.Value = false;
|
||||
|
||||
this.state = state;
|
||||
|
||||
if (this.IsCurrentScreen())
|
||||
Schedule(attemptStart);
|
||||
else
|
||||
newStatePending = true;
|
||||
}
|
||||
|
||||
public override void OnResuming(IScreen last)
|
||||
{
|
||||
base.OnResuming(last);
|
||||
|
||||
if (newStatePending)
|
||||
{
|
||||
attemptStart();
|
||||
newStatePending = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void userFinishedPlaying(int userId, SpectatorState state)
|
||||
{
|
||||
if (userId != targetUser.Id)
|
||||
return;
|
||||
|
||||
lock (scoreLock)
|
||||
{
|
||||
if (score != null)
|
||||
{
|
||||
score.Replay.HasReceivedAllFrames = true;
|
||||
score = null;
|
||||
}
|
||||
}
|
||||
|
||||
Schedule(clearDisplay);
|
||||
clearDisplay();
|
||||
}
|
||||
|
||||
private void clearDisplay()
|
||||
{
|
||||
watchButton.Enabled.Value = false;
|
||||
onlineBeatmapRequest?.Cancel();
|
||||
beatmapPanelContainer.Clear();
|
||||
previewTrackManager.StopAnyPlaying(this);
|
||||
}
|
||||
|
||||
private void attemptStart()
|
||||
private ScheduledDelegate scheduledStart;
|
||||
|
||||
private void scheduleStart(GameplayState gameplayState)
|
||||
{
|
||||
clearDisplay();
|
||||
showBeatmapPanel(state);
|
||||
|
||||
var resolvedRuleset = rulesets.AvailableRulesets.FirstOrDefault(r => r.ID == state.RulesetID)?.CreateInstance();
|
||||
|
||||
// ruleset not available
|
||||
if (resolvedRuleset == null)
|
||||
return;
|
||||
|
||||
if (state.BeatmapID == null)
|
||||
return;
|
||||
|
||||
var resolvedBeatmap = beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == state.BeatmapID);
|
||||
|
||||
if (resolvedBeatmap == null)
|
||||
// This function may be called multiple times in quick succession once the screen becomes current again.
|
||||
scheduledStart?.Cancel();
|
||||
scheduledStart = Schedule(() =>
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (this.IsCurrentScreen())
|
||||
start();
|
||||
else
|
||||
scheduleStart(gameplayState);
|
||||
});
|
||||
|
||||
lock (scoreLock)
|
||||
void start()
|
||||
{
|
||||
score = new Score
|
||||
{
|
||||
ScoreInfo = new ScoreInfo
|
||||
{
|
||||
Beatmap = resolvedBeatmap,
|
||||
User = targetUser,
|
||||
Mods = state.Mods.Select(m => m.ToMod(resolvedRuleset)).ToArray(),
|
||||
Ruleset = resolvedRuleset.RulesetInfo,
|
||||
},
|
||||
Replay = new Replay { HasReceivedAllFrames = false },
|
||||
};
|
||||
Beatmap.Value = gameplayState.Beatmap;
|
||||
Ruleset.Value = gameplayState.Ruleset.RulesetInfo;
|
||||
|
||||
ruleset.Value = resolvedRuleset.RulesetInfo;
|
||||
rulesetInstance = resolvedRuleset;
|
||||
|
||||
beatmap.Value = beatmaps.GetWorkingBeatmap(resolvedBeatmap);
|
||||
watchButton.Enabled.Value = true;
|
||||
|
||||
this.Push(new SpectatorPlayerLoader(score));
|
||||
this.Push(new SpectatorPlayerLoader(gameplayState.Score));
|
||||
}
|
||||
}
|
||||
|
||||
private void showBeatmapPanel(SpectatorState state)
|
||||
{
|
||||
if (state?.BeatmapID == null)
|
||||
{
|
||||
onlineBeatmap = null;
|
||||
return;
|
||||
}
|
||||
Debug.Assert(state.BeatmapID != null);
|
||||
|
||||
var req = new GetBeatmapSetRequest(state.BeatmapID.Value, BeatmapSetLookupType.BeatmapId);
|
||||
req.Success += res => Schedule(() =>
|
||||
onlineBeatmapRequest = new GetBeatmapSetRequest(state.BeatmapID.Value, BeatmapSetLookupType.BeatmapId);
|
||||
onlineBeatmapRequest.Success += res => Schedule(() =>
|
||||
{
|
||||
if (state != this.state)
|
||||
return;
|
||||
|
||||
onlineBeatmap = res.ToBeatmapSet(rulesets);
|
||||
beatmapPanelContainer.Child = new GridBeatmapPanel(onlineBeatmap);
|
||||
checkForAutomaticDownload();
|
||||
});
|
||||
|
||||
api.Queue(req);
|
||||
api.Queue(onlineBeatmapRequest);
|
||||
}
|
||||
|
||||
private void checkForAutomaticDownload()
|
||||
@ -369,21 +249,5 @@ namespace osu.Game.Screens.Play
|
||||
previewTrackManager.StopAnyPlaying(this);
|
||||
return base.OnExiting(next);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (spectatorStreaming != null)
|
||||
{
|
||||
spectatorStreaming.OnUserBeganPlaying -= userBeganPlaying;
|
||||
spectatorStreaming.OnUserFinishedPlaying -= userFinishedPlaying;
|
||||
spectatorStreaming.OnNewFrames -= userSentFrames;
|
||||
|
||||
spectatorStreaming.StopWatchingUser(targetUser.Id);
|
||||
}
|
||||
|
||||
managerUpdated?.UnbindAll();
|
||||
}
|
||||
}
|
||||
}
|
37
osu.Game/Screens/Spectate/GameplayState.cs
Normal file
37
osu.Game/Screens/Spectate/GameplayState.cs
Normal file
@ -0,0 +1,37 @@
|
||||
// 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.Beatmaps;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Scoring;
|
||||
|
||||
namespace osu.Game.Screens.Spectate
|
||||
{
|
||||
/// <summary>
|
||||
/// The gameplay state of a spectated user. This class is immutable.
|
||||
/// </summary>
|
||||
public class GameplayState
|
||||
{
|
||||
/// <summary>
|
||||
/// The score which the user is playing.
|
||||
/// </summary>
|
||||
public readonly Score Score;
|
||||
|
||||
/// <summary>
|
||||
/// The ruleset which the user is playing.
|
||||
/// </summary>
|
||||
public readonly Ruleset Ruleset;
|
||||
|
||||
/// <summary>
|
||||
/// The beatmap which the user is playing.
|
||||
/// </summary>
|
||||
public readonly WorkingBeatmap Beatmap;
|
||||
|
||||
public GameplayState(Score score, Ruleset ruleset, WorkingBeatmap beatmap)
|
||||
{
|
||||
Score = score;
|
||||
Ruleset = ruleset;
|
||||
Beatmap = beatmap;
|
||||
}
|
||||
}
|
||||
}
|
238
osu.Game/Screens/Spectate/SpectatorScreen.cs
Normal file
238
osu.Game/Screens/Spectate/SpectatorScreen.cs
Normal file
@ -0,0 +1,238 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Online.Spectator;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Rulesets.Replays.Types;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Screens.Spectate
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="OsuScreen"/> which spectates one or more users.
|
||||
/// </summary>
|
||||
public abstract class SpectatorScreen : OsuScreen
|
||||
{
|
||||
private readonly int[] userIds;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapManager beatmaps { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private SpectatorStreamingClient spectatorClient { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private UserLookupCache userLookupCache { get; set; }
|
||||
|
||||
// A lock is used to synchronise access to spectator/gameplay states, since this class is a screen which may become non-current and stop receiving updates at any point.
|
||||
private readonly object stateLock = new object();
|
||||
|
||||
private readonly Dictionary<int, User> userMap = new Dictionary<int, User>();
|
||||
private readonly Dictionary<int, SpectatorState> spectatorStates = new Dictionary<int, SpectatorState>();
|
||||
private readonly Dictionary<int, GameplayState> gameplayStates = new Dictionary<int, GameplayState>();
|
||||
|
||||
private IBindable<WeakReference<BeatmapSetInfo>> managerUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="SpectatorScreen"/>.
|
||||
/// </summary>
|
||||
/// <param name="userIds">The users to spectate.</param>
|
||||
protected SpectatorScreen(params int[] userIds)
|
||||
{
|
||||
this.userIds = userIds;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
spectatorClient.OnUserBeganPlaying += userBeganPlaying;
|
||||
spectatorClient.OnUserFinishedPlaying += userFinishedPlaying;
|
||||
spectatorClient.OnNewFrames += userSentFrames;
|
||||
|
||||
foreach (var id in userIds)
|
||||
{
|
||||
userLookupCache.GetUserAsync(id).ContinueWith(u => Schedule(() =>
|
||||
{
|
||||
if (u.Result == null)
|
||||
return;
|
||||
|
||||
lock (stateLock)
|
||||
userMap[id] = u.Result;
|
||||
|
||||
spectatorClient.WatchUser(id);
|
||||
}), TaskContinuationOptions.OnlyOnRanToCompletion);
|
||||
}
|
||||
|
||||
managerUpdated = beatmaps.ItemUpdated.GetBoundCopy();
|
||||
managerUpdated.BindValueChanged(beatmapUpdated);
|
||||
}
|
||||
|
||||
private void beatmapUpdated(ValueChangedEvent<WeakReference<BeatmapSetInfo>> e)
|
||||
{
|
||||
if (!e.NewValue.TryGetTarget(out var beatmapSet))
|
||||
return;
|
||||
|
||||
lock (stateLock)
|
||||
{
|
||||
foreach (var (userId, state) in spectatorStates)
|
||||
{
|
||||
if (beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID == state.BeatmapID))
|
||||
updateGameplayState(userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void userBeganPlaying(int userId, SpectatorState state)
|
||||
{
|
||||
if (state.RulesetID == null || state.BeatmapID == null)
|
||||
return;
|
||||
|
||||
lock (stateLock)
|
||||
{
|
||||
if (!userMap.ContainsKey(userId))
|
||||
return;
|
||||
|
||||
spectatorStates[userId] = state;
|
||||
Schedule(() => OnUserStateChanged(userId, state));
|
||||
|
||||
updateGameplayState(userId);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateGameplayState(int userId)
|
||||
{
|
||||
lock (stateLock)
|
||||
{
|
||||
Debug.Assert(userMap.ContainsKey(userId));
|
||||
|
||||
var spectatorState = spectatorStates[userId];
|
||||
var user = userMap[userId];
|
||||
|
||||
var resolvedRuleset = rulesets.AvailableRulesets.FirstOrDefault(r => r.ID == spectatorState.RulesetID)?.CreateInstance();
|
||||
if (resolvedRuleset == null)
|
||||
return;
|
||||
|
||||
var resolvedBeatmap = beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == spectatorState.BeatmapID);
|
||||
if (resolvedBeatmap == null)
|
||||
return;
|
||||
|
||||
var score = new Score
|
||||
{
|
||||
ScoreInfo = new ScoreInfo
|
||||
{
|
||||
Beatmap = resolvedBeatmap,
|
||||
User = user,
|
||||
Mods = spectatorState.Mods.Select(m => m.ToMod(resolvedRuleset)).ToArray(),
|
||||
Ruleset = resolvedRuleset.RulesetInfo,
|
||||
},
|
||||
Replay = new Replay { HasReceivedAllFrames = false },
|
||||
};
|
||||
|
||||
var gameplayState = new GameplayState(score, resolvedRuleset, beatmaps.GetWorkingBeatmap(resolvedBeatmap));
|
||||
|
||||
gameplayStates[userId] = gameplayState;
|
||||
Schedule(() => StartGameplay(userId, gameplayState));
|
||||
}
|
||||
}
|
||||
|
||||
private void userSentFrames(int userId, FrameDataBundle bundle)
|
||||
{
|
||||
lock (stateLock)
|
||||
{
|
||||
if (!userMap.ContainsKey(userId))
|
||||
return;
|
||||
|
||||
if (!gameplayStates.TryGetValue(userId, out var gameplayState))
|
||||
return;
|
||||
|
||||
// The ruleset instance should be guaranteed to be in sync with the score via ScoreLock.
|
||||
Debug.Assert(gameplayState.Ruleset != null && gameplayState.Ruleset.RulesetInfo.Equals(gameplayState.Score.ScoreInfo.Ruleset));
|
||||
|
||||
foreach (var frame in bundle.Frames)
|
||||
{
|
||||
IConvertibleReplayFrame convertibleFrame = gameplayState.Ruleset.CreateConvertibleReplayFrame();
|
||||
convertibleFrame.FromLegacy(frame, gameplayState.Beatmap.Beatmap);
|
||||
|
||||
var convertedFrame = (ReplayFrame)convertibleFrame;
|
||||
convertedFrame.Time = frame.Time;
|
||||
|
||||
gameplayState.Score.Replay.Frames.Add(convertedFrame);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void userFinishedPlaying(int userId, SpectatorState state)
|
||||
{
|
||||
lock (stateLock)
|
||||
{
|
||||
if (!userMap.ContainsKey(userId))
|
||||
return;
|
||||
|
||||
if (!gameplayStates.TryGetValue(userId, out var gameplayState))
|
||||
return;
|
||||
|
||||
gameplayState.Score.Replay.HasReceivedAllFrames = true;
|
||||
|
||||
gameplayStates.Remove(userId);
|
||||
Schedule(() => EndGameplay(userId));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a spectated user's state has changed.
|
||||
/// </summary>
|
||||
/// <param name="userId">The user whose state has changed.</param>
|
||||
/// <param name="spectatorState">The new state.</param>
|
||||
protected abstract void OnUserStateChanged(int userId, [NotNull] SpectatorState spectatorState);
|
||||
|
||||
/// <summary>
|
||||
/// Starts gameplay for a user.
|
||||
/// </summary>
|
||||
/// <param name="userId">The user to start gameplay for.</param>
|
||||
/// <param name="gameplayState">The gameplay state.</param>
|
||||
protected abstract void StartGameplay(int userId, [NotNull] GameplayState gameplayState);
|
||||
|
||||
/// <summary>
|
||||
/// Ends gameplay for a user.
|
||||
/// </summary>
|
||||
/// <param name="userId">The user to end gameplay for.</param>
|
||||
protected abstract void EndGameplay(int userId);
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (spectatorClient != null)
|
||||
{
|
||||
spectatorClient.OnUserBeganPlaying -= userBeganPlaying;
|
||||
spectatorClient.OnUserFinishedPlaying -= userFinishedPlaying;
|
||||
spectatorClient.OnNewFrames -= userSentFrames;
|
||||
|
||||
lock (stateLock)
|
||||
{
|
||||
foreach (var (userId, _) in userMap)
|
||||
spectatorClient.StopWatchingUser(userId);
|
||||
}
|
||||
}
|
||||
|
||||
managerUpdated?.UnbindAll();
|
||||
}
|
||||
}
|
||||
}
|
@ -45,7 +45,7 @@ namespace osu.Game.Users
|
||||
public double Accuracy;
|
||||
|
||||
[JsonIgnore]
|
||||
public string DisplayAccuracy => Accuracy.FormatAccuracy();
|
||||
public string DisplayAccuracy => (Accuracy / 100).FormatAccuracy();
|
||||
|
||||
[JsonProperty(@"play_count")]
|
||||
public int PlayCount;
|
||||
|
@ -29,7 +29,7 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.NETCore.Targets" Version="3.1.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2021.329.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2021.402.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
|
||||
<PackageReference Include="Sentry" Version="3.2.0" />
|
||||
<PackageReference Include="SharpCompress" Version="0.28.1" />
|
||||
|
@ -70,7 +70,7 @@
|
||||
<Reference Include="System.Net.Http" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.329.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.402.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
|
||||
</ItemGroup>
|
||||
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
|
||||
@ -93,7 +93,7 @@
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2021.329.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2021.402.0" />
|
||||
<PackageReference Include="SharpCompress" Version="0.28.1" />
|
||||
<PackageReference Include="NUnit" Version="3.12.0" />
|
||||
<PackageReference Include="SharpRaven" Version="2.4.0" />
|
||||
|
Loading…
Reference in New Issue
Block a user