1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 10:12:54 +08:00

Merge pull request #24885 from peppy/clock-fix-attempt-2

Adjust clock usage in line with framework changes
This commit is contained in:
Bartłomiej Dach 2023-10-06 20:43:24 +02:00 committed by GitHub
commit 037632940d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 68 additions and 147 deletions

View File

@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.922.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.1006.0" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.

View File

@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
[SetUp]
public void SetUp() => Schedule(() =>
{
gameplayClock = new GameplayClockContainer(manualClock)
gameplayClock = new GameplayClockContainer(manualClock, false, false)
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]

View File

@ -126,7 +126,7 @@ namespace osu.Game.Tests.Gameplay
public void TestSamplePlaybackWithBeatmapHitsoundsOff()
{
GameplayClockContainer gameplayContainer = null;
TestDrawableStoryboardSample sample = null;
DrawableStoryboardSample sample = null;
AddStep("disable beatmap hitsounds", () => config.SetValue(OsuSetting.BeatmapHitsounds, false));
@ -141,7 +141,7 @@ namespace osu.Game.Tests.Gameplay
Child = beatmapSkinSourceContainer
});
beatmapSkinSourceContainer.Add(sample = new TestDrawableStoryboardSample(new StoryboardSampleInfo("test-sample", 1, 1))
beatmapSkinSourceContainer.Add(sample = new DrawableStoryboardSample(new StoryboardSampleInfo("test-sample", 1, 1))
{
Clock = gameplayContainer
});
@ -199,14 +199,6 @@ namespace osu.Game.Tests.Gameplay
protected internal override ISkin GetSkin() => new TestSkin("test-sample", resources);
}
private partial class TestDrawableStoryboardSample : DrawableStoryboardSample
{
public TestDrawableStoryboardSample(StoryboardSampleInfo sampleInfo)
: base(sampleInfo)
{
}
}
#region IResourceStorageProvider
public IRenderer Renderer => host.Renderer;

View File

@ -3,6 +3,7 @@
using NUnit.Framework;
using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Timing;
using osu.Game.Screens.Play;
@ -16,16 +17,16 @@ namespace osu.Game.Tests.NonVisual
[TestCase(1)]
public void TestTrueGameplayRateWithGameplayAdjustment(double underlyingClockRate)
{
var framedClock = new FramedClock(new ManualClock { Rate = underlyingClockRate });
var gameplayClock = new TestGameplayClockContainer(framedClock);
var trackVirtual = new TrackVirtual(60000) { Frequency = { Value = underlyingClockRate } };
var gameplayClock = new TestGameplayClockContainer(trackVirtual);
Assert.That(gameplayClock.GetTrueGameplayRate(), Is.EqualTo(2));
}
private partial class TestGameplayClockContainer : GameplayClockContainer
{
public TestGameplayClockContainer(IFrameBasedClock underlyingClock)
: base(underlyingClock)
public TestGameplayClockContainer(IClock underlyingClock)
: base(underlyingClock, false, false)
{
AdjustmentsFromMods.AddAdjustment(AdjustableProperty.Frequency, new BindableDouble(2.0));
}

View File

@ -15,6 +15,9 @@ using osu.Game.Tests.Visual;
namespace osu.Game.Tests.OnlinePlay
{
// NOTE: This test scene never calls ProcessFrame on clocks.
// The current tests are fine without this as they are testing very static scenarios, but it's worth knowing
// if adding further tests to this class.
[HeadlessTest]
public partial class TestSceneCatchUpSyncManager : OsuTestScene
{
@ -28,7 +31,7 @@ namespace osu.Game.Tests.OnlinePlay
[SetUp]
public void Setup()
{
syncManager = new SpectatorSyncManager(master = new GameplayClockContainer(new TestManualClock()));
syncManager = new SpectatorSyncManager(master = new GameplayClockContainer(new TestManualClock(), false, false));
player1 = syncManager.CreateManagedClock();
player2 = syncManager.CreateManagedClock();
@ -188,6 +191,8 @@ namespace osu.Game.Tests.OnlinePlay
public void Reset()
{
IsRunning = false;
CurrentTime = 0;
}
public void ResetSpeedAdjustments()

View File

@ -63,7 +63,7 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("seek near end", () => EditorClock.Seek(EditorClock.TrackLength - 250));
AddUntilStep("clock stops", () => !EditorClock.IsRunning);
AddUntilStep("clock stopped at end", () => EditorClock.CurrentTime - EditorClock.TotalAppliedOffset, () => Is.EqualTo(EditorClock.TrackLength));
AddUntilStep("clock stopped at end", () => EditorClock.CurrentTime, () => Is.EqualTo(EditorClock.TrackLength));
AddStep("start clock again", () => EditorClock.Start());
AddAssert("clock looped to start", () => EditorClock.IsRunning && EditorClock.CurrentTime < 500);

View File

@ -6,11 +6,11 @@ using System.Diagnostics;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Configuration;
using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Mods;
@ -41,7 +41,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset());
[Cached(typeof(IGameplayClock))]
private readonly IGameplayClock gameplayClock = new GameplayClockContainer(new FramedClock());
private readonly IGameplayClock gameplayClock = new GameplayClockContainer(new TrackVirtual(60000), false, false);
// best way to check without exposing.
private Drawable hideTarget => hudOverlay.ChildrenOfType<SkinComponentsContainer>().First();

View File

@ -4,10 +4,10 @@
#nullable disable
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Overlays.SkinEditor;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset());
[Cached(typeof(IGameplayClock))]
private readonly IGameplayClock gameplayClock = new GameplayClockContainer(new FramedClock());
private readonly IGameplayClock gameplayClock = new GameplayClockContainer(new TrackVirtual(60000), false, false);
[Cached]
public readonly EditorClipboard Clipboard = new EditorClipboard();

View File

@ -8,11 +8,11 @@ using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset());
[Cached(typeof(IGameplayClock))]
private readonly IGameplayClock gameplayClock = new GameplayClockContainer(new FramedClock());
private readonly IGameplayClock gameplayClock = new GameplayClockContainer(new TrackVirtual(60000), false, false);
private IEnumerable<HUDOverlay> hudOverlays => CreatedDrawables.OfType<HUDOverlay>();

View File

@ -106,14 +106,12 @@ namespace osu.Game.Tests.Visual.Gameplay
if (storyboard != null)
storyboardContainer.Remove(storyboard, true);
var decoupledClock = new DecoupleableInterpolatingFramedClock { IsCoupled = true };
storyboardContainer.Clock = decoupledClock;
storyboardContainer.Clock = new FramedClock(Beatmap.Value.Track);
storyboard = toLoad.CreateDrawable(SelectedMods.Value);
storyboard.Passing = false;
storyboardContainer.Add(storyboard);
decoupledClock.ChangeSource(Beatmap.Value.Track);
}
private void loadStoryboard(string filename, Action<Storyboard>? setUpStoryboard = null)

View File

@ -5,7 +5,6 @@ using System;
using System.Diagnostics;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Timing;
@ -28,16 +27,6 @@ namespace osu.Game.Beatmaps
{
private readonly bool applyOffsets;
/// <summary>
/// The length of the underlying beatmap track. Will default to 60 seconds if unavailable.
/// </summary>
public double TrackLength => Track.Length;
/// <summary>
/// The underlying beatmap track, if available.
/// </summary>
public Track Track { get; private set; } = new TrackVirtual(60000);
/// <summary>
/// The total frequency adjustment from pause transforms. Should eventually be handled in a better way.
/// </summary>
@ -53,7 +42,7 @@ namespace osu.Game.Beatmaps
private IDisposable? beatmapOffsetSubscription;
private readonly DecoupleableInterpolatingFramedClock decoupledClock;
private readonly DecouplingFramedClock decoupledTrack;
[Resolved]
private OsuConfigManager config { get; set; } = null!;
@ -66,25 +55,21 @@ namespace osu.Game.Beatmaps
public bool IsRewinding { get; private set; }
public bool IsCoupled
{
get => decoupledClock.IsCoupled;
set => decoupledClock.IsCoupled = value;
}
public FramedBeatmapClock(bool applyOffsets = false)
public FramedBeatmapClock(bool applyOffsets, bool requireDecoupling, IClock? source = null)
{
this.applyOffsets = applyOffsets;
// A decoupled clock is used to ensure precise time values even when the host audio subsystem is not reporting
decoupledTrack = new DecouplingFramedClock(source) { AllowDecoupling = requireDecoupling };
// An interpolating clock is used to ensure precise time values even when the host audio subsystem is not reporting
// high precision times (on windows there's generally only 5-10ms reporting intervals, as an example).
decoupledClock = new DecoupleableInterpolatingFramedClock { IsCoupled = true };
var interpolatedTrack = new InterpolatingFramedClock(decoupledTrack);
if (applyOffsets)
{
// Audio timings in general with newer BASS versions don't match stable.
// This only seems to be required on windows. We need to eventually figure out why, with a bit of luck.
platformOffsetClock = new OffsetCorrectionClock(decoupledClock, ExternalPauseFrequencyAdjust) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 };
platformOffsetClock = new OffsetCorrectionClock(interpolatedTrack, ExternalPauseFrequencyAdjust) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 };
// User global offset (set in settings) should also be applied.
userGlobalOffsetClock = new OffsetCorrectionClock(platformOffsetClock, ExternalPauseFrequencyAdjust);
@ -94,7 +79,7 @@ namespace osu.Game.Beatmaps
}
else
{
finalClockSource = decoupledClock;
finalClockSource = interpolatedTrack;
}
}
@ -110,6 +95,7 @@ namespace osu.Game.Beatmaps
userAudioOffset = config.GetBindable<double>(OsuSetting.AudioOffset);
userAudioOffset.BindValueChanged(offset => userGlobalOffsetClock.Offset = offset.NewValue, true);
// TODO: this doesn't update when using ChangeSource() to change beatmap.
beatmapOffsetSubscription = realm.SubscribeToPropertyChanged(
r => r.Find<BeatmapInfo>(beatmap.Value.BeatmapInfo.ID)?.UserSettings,
settings => settings.Offset,
@ -124,17 +110,7 @@ namespace osu.Game.Beatmaps
{
base.Update();
if (Source != null && Source is not IAdjustableClock && Source.CurrentTime < decoupledClock.CurrentTime - 100)
{
// InterpolatingFramedClock won't interpolate backwards unless its source has an ElapsedFrameTime.
// See https://github.com/ppy/osu-framework/blob/ba1385330cc501f34937e08257e586c84e35d772/osu.Framework/Timing/InterpolatingFramedClock.cs#L91-L93
// This is not always the case here when doing large seeks.
// (Of note, this is not an issue if the source is adjustable, as the source is seeked to be in time by DecoupleableInterpolatingFramedClock).
// Rather than trying to get around this by fixing the framework clock stack, let's work around it for now.
Seek(Source.CurrentTime);
}
else
finalClockSource.ProcessFrame();
finalClockSource.ProcessFrame();
if (Clock.ElapsedFrameTime != 0)
IsRewinding = Clock.ElapsedFrameTime < 0;
@ -157,46 +133,42 @@ namespace osu.Game.Beatmaps
#region Delegation of IAdjustableClock / ISourceChangeableClock to decoupled clock.
public void ChangeSource(IClock? source)
{
Track = source as Track ?? new TrackVirtual(60000);
decoupledClock.ChangeSource(source);
}
public void ChangeSource(IClock? source) => decoupledTrack.ChangeSource(source);
public IClock? Source => decoupledClock.Source;
public IClock Source => decoupledTrack.Source;
public void Reset()
{
decoupledClock.Reset();
decoupledTrack.Reset();
finalClockSource.ProcessFrame();
}
public void Start()
{
decoupledClock.Start();
decoupledTrack.Start();
finalClockSource.ProcessFrame();
}
public void Stop()
{
decoupledClock.Stop();
decoupledTrack.Stop();
finalClockSource.ProcessFrame();
}
public bool Seek(double position)
{
bool success = decoupledClock.Seek(position - TotalAppliedOffset);
bool success = decoupledTrack.Seek(position - TotalAppliedOffset);
finalClockSource.ProcessFrame();
return success;
}
public void ResetSpeedAdjustments() => decoupledClock.ResetSpeedAdjustments();
public void ResetSpeedAdjustments() => decoupledTrack.ResetSpeedAdjustments();
public double Rate
{
get => decoupledClock.Rate;
set => decoupledClock.Rate = value;
get => decoupledTrack.Rate;
set => decoupledTrack.Rate = value;
}
#endregion

View File

@ -215,7 +215,7 @@ namespace osu.Game
/// For now, this is used as a source specifically for beat synced components.
/// Going forward, it could potentially be used as the single source-of-truth for beatmap timing.
/// </summary>
private readonly FramedBeatmapClock beatmapClock = new FramedBeatmapClock(true);
private readonly FramedBeatmapClock beatmapClock = new FramedBeatmapClock(applyOffsets: true, requireDecoupling: false);
protected override Container<Drawable> Content => content;
@ -441,16 +441,7 @@ namespace osu.Game
}
}
private void onTrackChanged(WorkingBeatmap beatmap, TrackChangeDirection direction)
{
// FramedBeatmapClock uses a decoupled clock internally which will mutate the source if it is an `IAdjustableClock`.
// We don't want this for now, as the intention of beatmapClock is to be a read-only source for beat sync components.
//
// Encapsulating in a FramedClock will avoid any mutations.
var framedClock = new FramedClock(beatmap.Track);
beatmapClock.ChangeSource(framedClock);
}
private void onTrackChanged(WorkingBeatmap beatmap, TrackChangeDirection direction) => beatmapClock.ChangeSource(beatmap.Track);
protected virtual void InitialiseFonts()
{

View File

@ -862,7 +862,7 @@ namespace osu.Game.Screens.Edit
private void resetTrack(bool seekToStart = false)
{
Beatmap.Value.Track.Stop();
clock.Stop();
if (seekToStart)
{

View File

@ -54,7 +54,7 @@ namespace osu.Game.Screens.Edit
this.beatDivisor = beatDivisor ?? new BindableBeatDivisor();
underlyingClock = new FramedBeatmapClock(applyOffsets: true) { IsCoupled = false };
underlyingClock = new FramedBeatmapClock(applyOffsets: true, requireDecoupling: true);
AddInternal(underlyingClock);
}
@ -158,8 +158,6 @@ namespace osu.Game.Screens.Edit
public double CurrentTime => underlyingClock.CurrentTime;
public double TotalAppliedOffset => underlyingClock.TotalAppliedOffset;
public void Reset()
{
ClearTransforms();

View File

@ -36,7 +36,6 @@ namespace osu.Game.Screens.Menu
private Sample welcome;
private DecoupleableInterpolatingFramedClock decoupledClock;
private TrianglesIntroSequence intro;
public IntroTriangles([CanBeNull] Func<MainMenu> createNextScreen = null)
@ -59,18 +58,12 @@ namespace osu.Game.Screens.Menu
{
PrepareMenuLoad();
decoupledClock = new DecoupleableInterpolatingFramedClock
{
IsCoupled = false
};
if (UsingThemedIntro)
decoupledClock.ChangeSource(Track);
var decouplingClock = new DecouplingFramedClock(UsingThemedIntro ? Track : null);
LoadComponentAsync(intro = new TrianglesIntroSequence(logo, () => FadeInBackground())
{
RelativeSizeAxes = Axes.Both,
Clock = decoupledClock,
Clock = new InterpolatingFramedClock(decouplingClock),
LoadMenu = LoadMenu
}, _ =>
{
@ -94,7 +87,7 @@ namespace osu.Game.Screens.Menu
StartTrack();
// no-op for the case of themed intro, no harm in calling for both scenarios as a safety measure.
decoupledClock.Start();
decouplingClock.Start();
});
}
}

View File

@ -49,7 +49,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
protected override void Update()
{
// The player clock's running state is controlled externally, but the local pausing state needs to be updated to start/stop gameplay.
if (GameplayClockContainer.SourceClock.IsRunning)
if (GameplayClockContainer.IsRunning)
GameplayClockContainer.Start();
else
GameplayClockContainer.Stop();
@ -67,7 +67,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart)
{
var gameplayClockContainer = new GameplayClockContainer(spectatorPlayerClock);
// Importantly, we don't want to apply decoupling because SpectatorPlayerClock updates its IsRunning directly.
// If we applied decoupling, this state change wouldn't actually cause the clock to stop.
// TODO: Can we just use Start/Stop rather than this workaround, now that DecouplingClock is more sane?
var gameplayClockContainer = new GameplayClockContainer(spectatorPlayerClock, applyOffsets: false, requireDecoupling: false);
clockAdjustmentsFromMods.BindAdjustments(gameplayClockContainer.AdjustmentsFromMods);
return gameplayClockContainer;
}

View File

@ -23,11 +23,6 @@ namespace osu.Game.Screens.Play
public bool IsRewinding => GameplayClock.IsRewinding;
/// <summary>
/// The source clock. Should generally not be used for any timekeeping purposes.
/// </summary>
public IClock SourceClock { get; private set; }
/// <summary>
/// Invoked when a seek has been performed via <see cref="Seek"/>
/// </summary>
@ -60,15 +55,14 @@ namespace osu.Game.Screens.Play
/// </summary>
/// <param name="sourceClock">The source <see cref="IClock"/> used for timing.</param>
/// <param name="applyOffsets">Whether to apply platform, user and beatmap offsets to the mix.</param>
public GameplayClockContainer(IClock sourceClock, bool applyOffsets = false)
/// <param name="requireDecoupling">Whether decoupling logic should be applied on the source clock.</param>
public GameplayClockContainer(IClock sourceClock, bool applyOffsets, bool requireDecoupling)
{
SourceClock = sourceClock;
RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
GameplayClock = new FramedBeatmapClock(applyOffsets) { IsCoupled = false },
GameplayClock = new FramedBeatmapClock(applyOffsets, requireDecoupling, sourceClock),
Content
};
}
@ -83,8 +77,6 @@ namespace osu.Game.Screens.Play
isPaused.Value = false;
ensureSourceClockSet();
PrepareStart();
// The case which caused this to be added is FrameStabilityContainer, which manages its own current and elapsed time.
@ -153,28 +145,11 @@ namespace osu.Game.Screens.Play
Stop();
ensureSourceClockSet();
if (time != null)
StartTime = time.Value;
Seek(StartTime);
// This is a workaround for the fact that DecoupleableInterpolatingFramedClock doesn't seek the source
// if the source is not IsRunning. (see https://github.com/ppy/osu-framework/blob/2102638056dfcf85d21b4d85266d53b5dd018767/osu.Framework/Timing/DecoupleableInterpolatingFramedClock.cs#L209-L210)
// I hope to remove this once we knock some sense into clocks in general.
//
// Without this seek, the multiplayer spectator start sequence breaks:
// - Individual clients' clocks are never updated to their expected time
// - The sync manager thinks they are running behind
// - Gameplay doesn't start when it should (until a timeout occurs because nothing is happening for 10+ seconds)
//
// In addition, we use `CurrentTime` for this seek instead of `StartTime` as the above seek may have applied inherent
// offsets which need to be accounted for (ie. FramedBeatmapClock.TotalAppliedOffset).
//
// See https://github.com/ppy/osu/pull/24451/files/87fee001c786b29db34063ef3350e9a9f024d3ab#diff-28ca02979641e2d98a15fe5d5e806f56acf60ac100258a059fa72503b6cc54e8.
(SourceClock as IAdjustableClock)?.Seek(CurrentTime);
if (!wasPaused || startClock)
Start();
}
@ -183,20 +158,7 @@ namespace osu.Game.Screens.Play
/// Changes the source clock.
/// </summary>
/// <param name="sourceClock">The new source.</param>
protected void ChangeSource(IClock sourceClock) => GameplayClock.ChangeSource(SourceClock = sourceClock);
/// <summary>
/// Ensures that the <see cref="GameplayClock"/> is set to <see cref="SourceClock"/>, if it hasn't been given a source yet.
/// This is usually done before a seek to avoid accidentally seeking only the adjustable source in decoupled mode,
/// but not the actual source clock.
/// That will pretty much only happen on the very first call of this method, as the source clock is passed in the constructor,
/// but it is not yet set on the adjustable source there.
/// </summary>
private void ensureSourceClockSet()
{
if (GameplayClock.Source == null)
ChangeSource(SourceClock);
}
protected void ChangeSource(IClock sourceClock) => GameplayClock.ChangeSource(sourceClock);
#region IAdjustableClock

View File

@ -65,7 +65,7 @@ namespace osu.Game.Screens.Play
/// <param name="beatmap">The beatmap to be used for time and metadata references.</param>
/// <param name="skipTargetTime">The latest time which should be used when introducing gameplay. Will be used when skipping forward.</param>
public MasterGameplayClockContainer(WorkingBeatmap beatmap, double skipTargetTime)
: base(beatmap.Track, true)
: base(beatmap.Track, applyOffsets: true, requireDecoupling: true)
{
this.beatmap = beatmap;
this.skipTargetTime = skipTargetTime;
@ -187,7 +187,13 @@ namespace osu.Game.Screens.Play
public void StopUsingBeatmapClock()
{
removeSourceClockAdjustments();
ChangeSource(new TrackVirtual(beatmap.Track.Length));
var virtualTrack = new TrackVirtual(beatmap.Track.Length);
virtualTrack.Seek(CurrentTime);
if (IsRunning)
virtualTrack.Start();
ChangeSource(virtualTrack);
addSourceClockAdjustments();
}

View File

@ -36,7 +36,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Realm" Version="11.5.0" />
<PackageReference Include="ppy.osu.Framework" Version="2023.922.0" />
<PackageReference Include="ppy.osu.Framework" Version="2023.1006.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.1003.0" />
<PackageReference Include="Sentry" Version="3.39.1" />
<PackageReference Include="SharpCompress" Version="0.33.0" />

View File

@ -23,6 +23,6 @@
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.iOS" Version="2023.922.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2023.1006.0" />
</ItemGroup>
</Project>