1
0
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:
Dean Herbert 2021-04-05 22:07:41 +09:00
commit 38e95a0e73
49 changed files with 731 additions and 238 deletions

View File

@ -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>

View File

@ -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);
}
}
}
}

View File

@ -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());

View File

@ -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" />

View File

@ -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);

View File

@ -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);

View File

@ -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());

View File

@ -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;

View File

@ -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" />

View File

@ -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)

View File

@ -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);

View 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;
}
}
}

View File

@ -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

View File

@ -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();
}

View File

@ -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());

View File

@ -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" />

View File

@ -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;
}
}
}

View File

@ -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();
}

View File

@ -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;

View File

@ -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());

View File

@ -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" />

View File

@ -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}"
});
}
}
}

View File

@ -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()
{

View File

@ -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>();

View File

@ -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" />

View File

@ -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>

View File

@ -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)

View File

@ -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]

View File

@ -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))));

View File

@ -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)

View File

@ -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;

View File

@ -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 }
}
}

View File

@ -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;
}
}
}
}
}

View File

@ -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));

View File

@ -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 =>
{

View File

@ -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;

View File

@ -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;
}
}
}

View File

@ -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.

View File

@ -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

View File

@ -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,

View File

@ -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
},

View File

@ -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());
}
}

View File

@ -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)

View File

@ -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();
}
}
}

View 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;
}
}
}

View 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();
}
}
}

View File

@ -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;

View File

@ -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" />

View File

@ -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" />