mirror of
https://github.com/ppy/osu.git
synced 2025-01-28 09:02:58 +08:00
Merge branch 'master' into importer-returns-live
This commit is contained in:
commit
916342c837
1
.github/workflows/diffcalc.yml
vendored
1
.github/workflows/diffcalc.yml
vendored
@ -53,6 +53,7 @@ jobs:
|
|||||||
diffcalc:
|
diffcalc:
|
||||||
name: Run
|
name: Run
|
||||||
runs-on: self-hosted
|
runs-on: self-hosted
|
||||||
|
timeout-minutes: 1440
|
||||||
if: needs.metadata.outputs.continue == 'yes'
|
if: needs.metadata.outputs.continue == 'yes'
|
||||||
needs: metadata
|
needs: metadata
|
||||||
strategy:
|
strategy:
|
||||||
|
@ -51,11 +51,11 @@
|
|||||||
<Reference Include="Java.Interop" />
|
<Reference Include="Java.Interop" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.918.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1004.0" />
|
||||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.929.0" />
|
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.929.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Transitive Dependencies">
|
<ItemGroup Label="Transitive Dependencies">
|
||||||
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
|
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
|
||||||
<PackageReference Include="Realm" Version="10.5.0" />
|
<PackageReference Include="Realm" Version="10.6.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -4,13 +4,11 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Testing;
|
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
using osu.Game.Rulesets.Osu.Mods;
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Screens.Play;
|
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Tests.Mods
|
namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||||
@ -122,7 +120,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
|||||||
private bool checkSomeHit() => Player.ScoreProcessor.JudgedHits >= 4;
|
private bool checkSomeHit() => Player.ScoreProcessor.JudgedHits >= 4;
|
||||||
|
|
||||||
private bool objectWithIncreasedVisibilityHasIndex(int index)
|
private bool objectWithIncreasedVisibilityHasIndex(int index)
|
||||||
=> Player.Mods.Value.OfType<TestOsuModHidden>().Single().FirstObject == Player.ChildrenOfType<GameplayBeatmap>().Single().HitObjects[index];
|
=> Player.Mods.Value.OfType<TestOsuModHidden>().Single().FirstObject == Player.GameplayState.Beatmap.HitObjects[index];
|
||||||
|
|
||||||
private class TestOsuModHidden : OsuModHidden
|
private class TestOsuModHidden : OsuModHidden
|
||||||
{
|
{
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 241 KiB After Width: | Height: | Size: 26 KiB |
@ -0,0 +1,3 @@
|
|||||||
|
[General]
|
||||||
|
Version: latest
|
||||||
|
HitCircleOverlayAboveNumber: 0
|
@ -17,6 +17,7 @@ using osu.Framework.Testing.Input;
|
|||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Osu.Skinning;
|
using osu.Game.Rulesets.Osu.Skinning;
|
||||||
using osu.Game.Rulesets.Osu.UI.Cursor;
|
using osu.Game.Rulesets.Osu.UI.Cursor;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
@ -29,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
public class TestSceneGameplayCursor : OsuSkinnableTestScene
|
public class TestSceneGameplayCursor : OsuSkinnableTestScene
|
||||||
{
|
{
|
||||||
[Cached]
|
[Cached]
|
||||||
private GameplayBeatmap gameplayBeatmap;
|
private GameplayState gameplayState;
|
||||||
|
|
||||||
private OsuCursorContainer lastContainer;
|
private OsuCursorContainer lastContainer;
|
||||||
|
|
||||||
@ -40,7 +41,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
public TestSceneGameplayCursor()
|
public TestSceneGameplayCursor()
|
||||||
{
|
{
|
||||||
gameplayBeatmap = new GameplayBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo));
|
var ruleset = new OsuRuleset();
|
||||||
|
gameplayState = new GameplayState(CreateBeatmap(ruleset.RulesetInfo), ruleset, Array.Empty<Mod>());
|
||||||
|
|
||||||
AddStep("change background colour", () =>
|
AddStep("change background colour", () =>
|
||||||
{
|
{
|
||||||
@ -57,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
AddSliderStep("circle size", 0f, 10f, 0f, val =>
|
AddSliderStep("circle size", 0f, 10f, 0f, val =>
|
||||||
{
|
{
|
||||||
config.SetValue(OsuSetting.AutoCursorSize, true);
|
config.SetValue(OsuSetting.AutoCursorSize, true);
|
||||||
gameplayBeatmap.BeatmapInfo.BaseDifficulty.CircleSize = val;
|
gameplayState.Beatmap.BeatmapInfo.BaseDifficulty.CircleSize = val;
|
||||||
Scheduler.AddOnce(() => loadContent(false));
|
Scheduler.AddOnce(() => loadContent(false));
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -73,7 +75,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
public void TestSizing(int circleSize, float userScale)
|
public void TestSizing(int circleSize, float userScale)
|
||||||
{
|
{
|
||||||
AddStep($"set user scale to {userScale}", () => config.SetValue(OsuSetting.GameplayCursorSize, userScale));
|
AddStep($"set user scale to {userScale}", () => config.SetValue(OsuSetting.GameplayCursorSize, userScale));
|
||||||
AddStep($"adjust cs to {circleSize}", () => gameplayBeatmap.BeatmapInfo.BaseDifficulty.CircleSize = circleSize);
|
AddStep($"adjust cs to {circleSize}", () => gameplayState.Beatmap.BeatmapInfo.BaseDifficulty.CircleSize = circleSize);
|
||||||
AddStep("turn on autosizing", () => config.SetValue(OsuSetting.AutoCursorSize, true));
|
AddStep("turn on autosizing", () => config.SetValue(OsuSetting.AutoCursorSize, true));
|
||||||
|
|
||||||
AddStep("load content", () => loadContent());
|
AddStep("load content", () => loadContent());
|
||||||
|
@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
private OsuPlayfield playfield { get; set; }
|
private OsuPlayfield playfield { get; set; }
|
||||||
|
|
||||||
[Resolved(canBeNull: true)]
|
[Resolved(canBeNull: true)]
|
||||||
private GameplayBeatmap gameplayBeatmap { get; set; }
|
private GameplayState gameplayState { get; set; }
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(ISkinSource skin, OsuColour colours)
|
private void load(ISkinSource skin, OsuColour colours)
|
||||||
@ -75,12 +75,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
{
|
{
|
||||||
if (playfield == null || gameplayBeatmap == null) return;
|
if (playfield == null || gameplayState == null) return;
|
||||||
|
|
||||||
DrawableHitObject kiaiHitObject = null;
|
DrawableHitObject kiaiHitObject = null;
|
||||||
|
|
||||||
// Check whether currently in a kiai section first. This is only done as an optimisation to avoid enumerating AliveObjects when not necessary.
|
// Check whether currently in a kiai section first. This is only done as an optimisation to avoid enumerating AliveObjects when not necessary.
|
||||||
if (gameplayBeatmap.ControlPointInfo.EffectPointAt(Time.Current).KiaiMode)
|
if (gameplayState.Beatmap.ControlPointInfo.EffectPointAt(Time.Current).KiaiMode)
|
||||||
kiaiHitObject = playfield.HitObjectContainer.AliveObjects.FirstOrDefault(isTracking);
|
kiaiHitObject = playfield.HitObjectContainer.AliveObjects.FirstOrDefault(isTracking);
|
||||||
|
|
||||||
kiaiSpewer.Active.Value = kiaiHitObject != null;
|
kiaiSpewer.Active.Value = kiaiHitObject != null;
|
||||||
|
@ -35,8 +35,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
|
|
||||||
private Drawable hitCircleSprite;
|
private Drawable hitCircleSprite;
|
||||||
|
|
||||||
protected Drawable HitCircleOverlay { get; private set; }
|
protected Container OverlayLayer { get; private set; }
|
||||||
|
|
||||||
|
private Drawable hitCircleOverlay;
|
||||||
private SkinnableSpriteText hitCircleText;
|
private SkinnableSpriteText hitCircleText;
|
||||||
|
|
||||||
private readonly Bindable<Color4> accentColour = new Bindable<Color4>();
|
private readonly Bindable<Color4> accentColour = new Bindable<Color4>();
|
||||||
@ -78,17 +79,22 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
},
|
},
|
||||||
HitCircleOverlay = new KiaiFlashingSprite
|
OverlayLayer = new Container
|
||||||
{
|
{
|
||||||
Texture = overlayTexture,
|
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
},
|
Child = hitCircleOverlay = new KiaiFlashingSprite
|
||||||
|
{
|
||||||
|
Texture = overlayTexture,
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
},
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (hasNumber)
|
if (hasNumber)
|
||||||
{
|
{
|
||||||
AddInternal(hitCircleText = new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText
|
OverlayLayer.Add(hitCircleText = new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText
|
||||||
{
|
{
|
||||||
Font = OsuFont.Numeric.With(size: 40),
|
Font = OsuFont.Numeric.With(size: 40),
|
||||||
UseFullGlyphHeight = false,
|
UseFullGlyphHeight = false,
|
||||||
@ -102,7 +108,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
bool overlayAboveNumber = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true;
|
bool overlayAboveNumber = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true;
|
||||||
|
|
||||||
if (overlayAboveNumber)
|
if (overlayAboveNumber)
|
||||||
ChangeInternalChildDepth(HitCircleOverlay, float.MinValue);
|
OverlayLayer.ChangeChildDepth(hitCircleOverlay, float.MinValue);
|
||||||
|
|
||||||
accentColour.BindTo(drawableObject.AccentColour);
|
accentColour.BindTo(drawableObject.AccentColour);
|
||||||
indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable);
|
indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable);
|
||||||
@ -147,8 +153,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
hitCircleSprite.FadeOut(legacy_fade_duration, Easing.Out);
|
hitCircleSprite.FadeOut(legacy_fade_duration, Easing.Out);
|
||||||
hitCircleSprite.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
|
hitCircleSprite.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
|
||||||
|
|
||||||
HitCircleOverlay.FadeOut(legacy_fade_duration, Easing.Out);
|
hitCircleOverlay.FadeOut(legacy_fade_duration, Easing.Out);
|
||||||
HitCircleOverlay.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
|
hitCircleOverlay.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
|
||||||
|
|
||||||
if (hasNumber)
|
if (hasNumber)
|
||||||
{
|
{
|
||||||
|
@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
[Resolved(canBeNull: true)]
|
[Resolved(canBeNull: true)]
|
||||||
private DrawableHitObject drawableHitObject { get; set; }
|
private DrawableHitObject drawableHitObject { get; set; }
|
||||||
|
|
||||||
private Drawable proxiedHitCircleOverlay;
|
private Drawable proxiedOverlayLayer;
|
||||||
|
|
||||||
public LegacySliderHeadHitCircle()
|
public LegacySliderHeadHitCircle()
|
||||||
: base("sliderstartcircle")
|
: base("sliderstartcircle")
|
||||||
@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
proxiedHitCircleOverlay = HitCircleOverlay.CreateProxy();
|
proxiedOverlayLayer = OverlayLayer.CreateProxy();
|
||||||
|
|
||||||
if (drawableHitObject != null)
|
if (drawableHitObject != null)
|
||||||
{
|
{
|
||||||
@ -35,11 +35,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
|
|
||||||
private void onHitObjectApplied(DrawableHitObject drawableObject)
|
private void onHitObjectApplied(DrawableHitObject drawableObject)
|
||||||
{
|
{
|
||||||
Debug.Assert(proxiedHitCircleOverlay.Parent == null);
|
Debug.Assert(proxiedOverlayLayer.Parent == null);
|
||||||
|
|
||||||
// see logic in LegacyReverseArrow.
|
// see logic in LegacyReverseArrow.
|
||||||
(drawableObject as DrawableSliderHead)?.DrawableSlider
|
(drawableObject as DrawableSliderHead)?.DrawableSlider
|
||||||
.OverlayElementContainer.Add(proxiedHitCircleOverlay.With(d => d.Depth = float.MinValue));
|
.OverlayElementContainer.Add(proxiedOverlayLayer.With(d => d.Depth = float.MinValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
protected override void Dispose(bool isDisposing)
|
||||||
|
@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Resolved(canBeNull: true)]
|
[Resolved(canBeNull: true)]
|
||||||
private GameplayBeatmap beatmap { get; set; }
|
private GameplayState state { get; set; }
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private OsuConfigManager config { get; set; }
|
private OsuConfigManager config { get; set; }
|
||||||
@ -96,10 +96,10 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
{
|
{
|
||||||
float scale = userCursorScale.Value;
|
float scale = userCursorScale.Value;
|
||||||
|
|
||||||
if (autoCursorScale.Value && beatmap != null)
|
if (autoCursorScale.Value && state != null)
|
||||||
{
|
{
|
||||||
// if we have a beatmap available, let's get its circle size to figure out an automatic cursor scale modifier.
|
// if we have a beatmap available, let's get its circle size to figure out an automatic cursor scale modifier.
|
||||||
scale *= GetScaleForCircleSize(beatmap.BeatmapInfo.BaseDifficulty.CircleSize);
|
scale *= GetScaleForCircleSize(state.Beatmap.BeatmapInfo.BaseDifficulty.CircleSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
cursorScale.Value = scale;
|
cursorScale.Value = scale;
|
||||||
|
@ -25,10 +25,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
|||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader(true)]
|
[BackgroundDependencyLoader(true)]
|
||||||
private void load(GameplayBeatmap gameplayBeatmap)
|
private void load(GameplayState gameplayState)
|
||||||
{
|
{
|
||||||
if (gameplayBeatmap != null)
|
if (gameplayState != null)
|
||||||
((IBindable<JudgementResult>)LastResult).BindTo(gameplayBeatmap.LastJudgementResult);
|
((IBindable<JudgementResult>)LastResult).BindTo(gameplayState.LastJudgementResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool passing;
|
private bool passing;
|
||||||
|
@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Taiko.UI
|
|||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader(true)]
|
[BackgroundDependencyLoader(true)]
|
||||||
private void load(TextureStore textures, GameplayBeatmap gameplayBeatmap)
|
private void load(TextureStore textures, GameplayState gameplayState)
|
||||||
{
|
{
|
||||||
InternalChildren = new[]
|
InternalChildren = new[]
|
||||||
{
|
{
|
||||||
@ -49,8 +49,8 @@ namespace osu.Game.Rulesets.Taiko.UI
|
|||||||
animations[TaikoMascotAnimationState.Fail] = new TaikoMascotAnimation(TaikoMascotAnimationState.Fail),
|
animations[TaikoMascotAnimationState.Fail] = new TaikoMascotAnimation(TaikoMascotAnimationState.Fail),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (gameplayBeatmap != null)
|
if (gameplayState != null)
|
||||||
((IBindable<JudgementResult>)LastResult).BindTo(gameplayBeatmap.LastJudgementResult);
|
((IBindable<JudgementResult>)LastResult).BindTo(gameplayState.LastJudgementResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
|
@ -149,5 +149,32 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
|||||||
Assert.AreEqual(AnimationLoopType.LoopForever, ((StoryboardAnimation)foreground.Elements[5]).LoopType);
|
Assert.AreEqual(AnimationLoopType.LoopForever, ((StoryboardAnimation)foreground.Elements[5]).LoopType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDecodeLoopCount()
|
||||||
|
{
|
||||||
|
// all loop sequences in loop-count.osb have a total duration of 2000ms (fade in 0->1000ms, fade out 1000->2000ms).
|
||||||
|
const double loop_duration = 2000;
|
||||||
|
|
||||||
|
var decoder = new LegacyStoryboardDecoder();
|
||||||
|
|
||||||
|
using (var resStream = TestResources.OpenResource("loop-count.osb"))
|
||||||
|
using (var stream = new LineBufferedReader(resStream))
|
||||||
|
{
|
||||||
|
var storyboard = decoder.Decode(stream);
|
||||||
|
|
||||||
|
StoryboardLayer background = storyboard.Layers.Single(l => l.Depth == 3);
|
||||||
|
|
||||||
|
// stable ensures that any loop command executes at least once, even if the loop count specified in the .osb is zero or negative.
|
||||||
|
StoryboardSprite zeroTimes = background.Elements.OfType<StoryboardSprite>().Single(s => s.Path == "zero-times.png");
|
||||||
|
Assert.That(zeroTimes.EndTime, Is.EqualTo(1000 + loop_duration));
|
||||||
|
|
||||||
|
StoryboardSprite oneTime = background.Elements.OfType<StoryboardSprite>().Single(s => s.Path == "one-time.png");
|
||||||
|
Assert.That(oneTime.EndTime, Is.EqualTo(4000 + loop_duration));
|
||||||
|
|
||||||
|
StoryboardSprite manyTimes = background.Elements.OfType<StoryboardSprite>().Single(s => s.Path == "many-times.png");
|
||||||
|
Assert.That(manyTimes.EndTime, Is.EqualTo(9000 + 40 * loop_duration));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
64
osu.Game.Tests/Database/GeneralUsageTests.cs
Normal file
64
osu.Game.Tests/Database/GeneralUsageTests.cs
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using NUnit.Framework;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Database
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class GeneralUsageTests : RealmTest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Just test the construction of a new database works.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void TestConstructRealm()
|
||||||
|
{
|
||||||
|
RunTestWithRealm((realmFactory, _) => { realmFactory.CreateContext().Refresh(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestBlockOperations()
|
||||||
|
{
|
||||||
|
RunTestWithRealm((realmFactory, _) =>
|
||||||
|
{
|
||||||
|
using (realmFactory.BlockAllOperations())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestBlockOperationsWithContention()
|
||||||
|
{
|
||||||
|
RunTestWithRealm((realmFactory, _) =>
|
||||||
|
{
|
||||||
|
ManualResetEventSlim stopThreadedUsage = new ManualResetEventSlim();
|
||||||
|
ManualResetEventSlim hasThreadedUsage = new ManualResetEventSlim();
|
||||||
|
|
||||||
|
Task.Factory.StartNew(() =>
|
||||||
|
{
|
||||||
|
using (realmFactory.CreateContext())
|
||||||
|
{
|
||||||
|
hasThreadedUsage.Set();
|
||||||
|
|
||||||
|
stopThreadedUsage.Wait();
|
||||||
|
}
|
||||||
|
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler);
|
||||||
|
|
||||||
|
hasThreadedUsage.Wait();
|
||||||
|
|
||||||
|
Assert.Throws<TimeoutException>(() =>
|
||||||
|
{
|
||||||
|
using (realmFactory.BlockAllOperations())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
stopThreadedUsage.Set();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
83
osu.Game.Tests/Database/RealmTest.cs
Normal file
83
osu.Game.Tests/Database/RealmTest.cs
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
// 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.Runtime.CompilerServices;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Nito.AsyncEx;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Logging;
|
||||||
|
using osu.Framework.Platform;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Database;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Database
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public abstract class RealmTest
|
||||||
|
{
|
||||||
|
private static readonly TemporaryNativeStorage storage;
|
||||||
|
|
||||||
|
static RealmTest()
|
||||||
|
{
|
||||||
|
storage = new TemporaryNativeStorage("realm-test");
|
||||||
|
storage.DeleteDirectory(string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void RunTestWithRealm(Action<RealmContextFactory, Storage> testAction, [CallerMemberName] string caller = "")
|
||||||
|
{
|
||||||
|
AsyncContext.Run(() =>
|
||||||
|
{
|
||||||
|
var testStorage = storage.GetStorageForDirectory(caller);
|
||||||
|
|
||||||
|
using (var realmFactory = new RealmContextFactory(testStorage, caller))
|
||||||
|
{
|
||||||
|
Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}");
|
||||||
|
testAction(realmFactory, testStorage);
|
||||||
|
|
||||||
|
realmFactory.Dispose();
|
||||||
|
|
||||||
|
Logger.Log($"Final database size: {getFileSize(testStorage, realmFactory)}");
|
||||||
|
realmFactory.Compact();
|
||||||
|
Logger.Log($"Final database size after compact: {getFileSize(testStorage, realmFactory)}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void RunTestWithRealmAsync(Func<RealmContextFactory, Storage, Task> testAction, [CallerMemberName] string caller = "")
|
||||||
|
{
|
||||||
|
AsyncContext.Run(async () =>
|
||||||
|
{
|
||||||
|
var testStorage = storage.GetStorageForDirectory(caller);
|
||||||
|
|
||||||
|
using (var realmFactory = new RealmContextFactory(testStorage, caller))
|
||||||
|
{
|
||||||
|
Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}");
|
||||||
|
await testAction(realmFactory, testStorage);
|
||||||
|
|
||||||
|
realmFactory.Dispose();
|
||||||
|
|
||||||
|
Logger.Log($"Final database size: {getFileSize(testStorage, realmFactory)}");
|
||||||
|
realmFactory.Compact();
|
||||||
|
Logger.Log($"Final database size after compact: {getFileSize(testStorage, realmFactory)}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long getFileSize(Storage testStorage, RealmContextFactory realmFactory)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var stream = testStorage.GetStream(realmFactory.Filename))
|
||||||
|
return stream?.Length ?? 0;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// windows runs may error due to file still being open.
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -32,7 +32,7 @@ namespace osu.Game.Tests.Database
|
|||||||
|
|
||||||
storage = new NativeStorage(directory.FullName);
|
storage = new NativeStorage(directory.FullName);
|
||||||
|
|
||||||
realmContextFactory = new RealmContextFactory(storage);
|
realmContextFactory = new RealmContextFactory(storage, "test");
|
||||||
keyBindingStore = new RealmKeyBindingStore(realmContextFactory);
|
keyBindingStore = new RealmKeyBindingStore(realmContextFactory);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,9 +53,9 @@ namespace osu.Game.Tests.Database
|
|||||||
|
|
||||||
private int queryCount(GlobalAction? match = null)
|
private int queryCount(GlobalAction? match = null)
|
||||||
{
|
{
|
||||||
using (var usage = realmContextFactory.GetForRead())
|
using (var realm = realmContextFactory.CreateContext())
|
||||||
{
|
{
|
||||||
var results = usage.Realm.All<RealmKeyBinding>();
|
var results = realm.All<RealmKeyBinding>();
|
||||||
if (match.HasValue)
|
if (match.HasValue)
|
||||||
results = results.Where(k => k.ActionInt == (int)match.Value);
|
results = results.Where(k => k.ActionInt == (int)match.Value);
|
||||||
return results.Count();
|
return results.Count();
|
||||||
@ -69,26 +69,24 @@ namespace osu.Game.Tests.Database
|
|||||||
|
|
||||||
keyBindingStore.Register(testContainer, Enumerable.Empty<RulesetInfo>());
|
keyBindingStore.Register(testContainer, Enumerable.Empty<RulesetInfo>());
|
||||||
|
|
||||||
using (var primaryUsage = realmContextFactory.GetForRead())
|
using (var primaryRealm = realmContextFactory.CreateContext())
|
||||||
{
|
{
|
||||||
var backBinding = primaryUsage.Realm.All<RealmKeyBinding>().Single(k => k.ActionInt == (int)GlobalAction.Back);
|
var backBinding = primaryRealm.All<RealmKeyBinding>().Single(k => k.ActionInt == (int)GlobalAction.Back);
|
||||||
|
|
||||||
Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.Escape }));
|
Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.Escape }));
|
||||||
|
|
||||||
var tsr = ThreadSafeReference.Create(backBinding);
|
var tsr = ThreadSafeReference.Create(backBinding);
|
||||||
|
|
||||||
using (var usage = realmContextFactory.GetForWrite())
|
using (var threadedContext = realmContextFactory.CreateContext())
|
||||||
{
|
{
|
||||||
var binding = usage.Realm.ResolveReference(tsr);
|
var binding = threadedContext.ResolveReference(tsr);
|
||||||
binding.KeyCombination = new KeyCombination(InputKey.BackSpace);
|
threadedContext.Write(() => binding.KeyCombination = new KeyCombination(InputKey.BackSpace));
|
||||||
|
|
||||||
usage.Commit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace }));
|
Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace }));
|
||||||
|
|
||||||
// check still correct after re-query.
|
// check still correct after re-query.
|
||||||
backBinding = primaryUsage.Realm.All<RealmKeyBinding>().Single(k => k.ActionInt == (int)GlobalAction.Back);
|
backBinding = primaryRealm.All<RealmKeyBinding>().Single(k => k.ActionInt == (int)GlobalAction.Back);
|
||||||
Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace }));
|
Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
15
osu.Game.Tests/Resources/loop-count.osb
Normal file
15
osu.Game.Tests/Resources/loop-count.osb
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
osu file format v14
|
||||||
|
|
||||||
|
[Events]
|
||||||
|
Sprite,Background,TopCentre,"zero-times.png",320,240
|
||||||
|
L,1000,0
|
||||||
|
F,0,0,1000,0,1
|
||||||
|
F,0,1000,2000,1,0
|
||||||
|
Sprite,Background,TopCentre,"one-time.png",320,240
|
||||||
|
L,4000,1
|
||||||
|
F,0,0,1000,0,1
|
||||||
|
F,0,1000,2000,1,0
|
||||||
|
Sprite,Background,TopCentre,"many-times.png",320,240
|
||||||
|
L,9000,40
|
||||||
|
F,0,0,1000,0,1
|
||||||
|
F,0,1000,2000,1,0
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
@ -17,6 +18,8 @@ using osu.Game.Beatmaps;
|
|||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
using osu.Game.Replays;
|
using osu.Game.Replays;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Rulesets.Replays;
|
using osu.Game.Rulesets.Replays;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
@ -38,7 +41,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
private TestReplayRecorder recorder;
|
private TestReplayRecorder recorder;
|
||||||
|
|
||||||
[Cached]
|
[Cached]
|
||||||
private GameplayBeatmap gameplayBeatmap = new GameplayBeatmap(new Beatmap());
|
private GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), Array.Empty<Mod>());
|
||||||
|
|
||||||
[SetUp]
|
[SetUp]
|
||||||
public void SetUp() => Schedule(() =>
|
public void SetUp() => Schedule(() =>
|
||||||
@ -57,7 +60,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
Recorder = recorder = new TestReplayRecorder(new Score
|
Recorder = recorder = new TestReplayRecorder(new Score
|
||||||
{
|
{
|
||||||
Replay = replay,
|
Replay = replay,
|
||||||
ScoreInfo = { Beatmap = gameplayBeatmap.BeatmapInfo }
|
ScoreInfo = { Beatmap = gameplayState.Beatmap.BeatmapInfo }
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos),
|
ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos),
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
@ -13,6 +14,8 @@ using osu.Game.Beatmaps;
|
|||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
using osu.Game.Replays;
|
using osu.Game.Replays;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Rulesets.Replays;
|
using osu.Game.Rulesets.Replays;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
@ -30,7 +33,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
private readonly TestRulesetInputManager recordingManager;
|
private readonly TestRulesetInputManager recordingManager;
|
||||||
|
|
||||||
[Cached]
|
[Cached]
|
||||||
private GameplayBeatmap gameplayBeatmap = new GameplayBeatmap(new Beatmap());
|
private GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), Array.Empty<Mod>());
|
||||||
|
|
||||||
public TestSceneReplayRecording()
|
public TestSceneReplayRecording()
|
||||||
{
|
{
|
||||||
@ -48,7 +51,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
Recorder = new TestReplayRecorder(new Score
|
Recorder = new TestReplayRecorder(new Score
|
||||||
{
|
{
|
||||||
Replay = replay,
|
Replay = replay,
|
||||||
ScoreInfo = { Beatmap = gameplayBeatmap.BeatmapInfo }
|
ScoreInfo = { Beatmap = gameplayState.Beatmap.BeatmapInfo }
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos)
|
ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos)
|
||||||
|
@ -25,6 +25,8 @@ using osu.Game.Online.Spectator;
|
|||||||
using osu.Game.Replays;
|
using osu.Game.Replays;
|
||||||
using osu.Game.Replays.Legacy;
|
using osu.Game.Replays.Legacy;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Rulesets.Replays;
|
using osu.Game.Rulesets.Replays;
|
||||||
using osu.Game.Rulesets.Replays.Types;
|
using osu.Game.Rulesets.Replays.Types;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
@ -62,7 +64,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
private SpectatorClient spectatorClient { get; set; }
|
private SpectatorClient spectatorClient { get; set; }
|
||||||
|
|
||||||
[Cached]
|
[Cached]
|
||||||
private GameplayBeatmap gameplayBeatmap = new GameplayBeatmap(new Beatmap());
|
private GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), Array.Empty<Mod>());
|
||||||
|
|
||||||
[SetUp]
|
[SetUp]
|
||||||
public void SetUp() => Schedule(() =>
|
public void SetUp() => Schedule(() =>
|
||||||
|
@ -7,6 +7,7 @@ using NUnit.Framework;
|
|||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Framework.Threading;
|
using osu.Framework.Threading;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Overlays.Settings.Sections.Input;
|
using osu.Game.Overlays.Settings.Sections.Input;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
@ -230,6 +231,22 @@ namespace osu.Game.Tests.Visual.Settings
|
|||||||
AddAssert("first binding selected", () => multiBindingRow.ChildrenOfType<KeyBindingRow.KeyButton>().First().IsBinding);
|
AddAssert("first binding selected", () => multiBindingRow.ChildrenOfType<KeyBindingRow.KeyButton>().First().IsBinding);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestFilteringHidesResetSectionButtons()
|
||||||
|
{
|
||||||
|
SearchTextBox searchTextBox = null;
|
||||||
|
|
||||||
|
AddStep("add any search term", () =>
|
||||||
|
{
|
||||||
|
searchTextBox = panel.ChildrenOfType<SearchTextBox>().Single();
|
||||||
|
searchTextBox.Current.Value = "chat";
|
||||||
|
});
|
||||||
|
AddUntilStep("all reset section bindings buttons hidden", () => panel.ChildrenOfType<ResetButton>().All(button => button.Alpha == 0));
|
||||||
|
|
||||||
|
AddStep("clear search term", () => searchTextBox.Current.Value = string.Empty);
|
||||||
|
AddUntilStep("all reset section bindings buttons shown", () => panel.ChildrenOfType<ResetButton>().All(button => button.Alpha == 1));
|
||||||
|
}
|
||||||
|
|
||||||
private void checkBinding(string name, string keyName)
|
private void checkBinding(string name, string keyName)
|
||||||
{
|
{
|
||||||
AddAssert($"Check {name} is bound to {keyName}", () =>
|
AddAssert($"Check {name} is bound to {keyName}", () =>
|
||||||
|
77
osu.Game.Tests/Visual/UserInterface/TestSceneOsuFont.cs
Normal file
77
osu.Game.Tests/Visual/UserInterface/TestSceneOsuFont.cs
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
// 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 NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Graphics.Sprites;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.UserInterface
|
||||||
|
{
|
||||||
|
public class TestSceneOsuFont : OsuTestScene
|
||||||
|
{
|
||||||
|
private OsuSpriteText spriteText;
|
||||||
|
|
||||||
|
private readonly BindableBool useAlternates = new BindableBool();
|
||||||
|
private readonly Bindable<FontWeight> weight = new Bindable<FontWeight>(FontWeight.Regular);
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
Child = spriteText = new OsuSpriteText
|
||||||
|
{
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AllowMultiline = true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
useAlternates.BindValueChanged(_ => updateFont());
|
||||||
|
weight.BindValueChanged(_ => updateFont(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateFont()
|
||||||
|
{
|
||||||
|
FontUsage usage = useAlternates.Value ? OsuFont.TorusAlternate : OsuFont.Torus;
|
||||||
|
spriteText.Font = usage.With(size: 40, weight: weight.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestTorusAlternates()
|
||||||
|
{
|
||||||
|
AddStep("set all ASCII letters", () => spriteText.Text = @"ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||||
|
abcdefghijklmnopqrstuvwxyz");
|
||||||
|
AddStep("set all alternates", () => spriteText.Text = @"A Á Ă Â Ä À Ā Ą Å Ã
|
||||||
|
Æ B D Ð Ď Đ E É Ě Ê
|
||||||
|
Ë Ė È Ē Ę F G Ğ Ģ Ġ
|
||||||
|
H I Í Î Ï İ Ì Ī Į K
|
||||||
|
Ķ O Œ P Þ Q R Ŕ Ř Ŗ
|
||||||
|
T Ŧ Ť Ţ Ț V W Ẃ Ŵ Ẅ
|
||||||
|
Ẁ X Y Ý Ŷ Ÿ Ỳ a á ă
|
||||||
|
â ä à ā ą å ã æ b d
|
||||||
|
ď đ e é ě ê ë ė è ē
|
||||||
|
ę f g ğ ģ ġ k ķ m n
|
||||||
|
ń ň ņ ŋ ñ o œ p þ q
|
||||||
|
t ŧ ť ţ ț u ú û ü ù
|
||||||
|
ű ū ų ů w ẃ ŵ ẅ ẁ x
|
||||||
|
y ý ŷ ÿ ỳ");
|
||||||
|
|
||||||
|
AddToggleStep("toggle alternates", alternates => useAlternates.Value = alternates);
|
||||||
|
|
||||||
|
addSetWeightStep(FontWeight.Light);
|
||||||
|
addSetWeightStep(FontWeight.Regular);
|
||||||
|
addSetWeightStep(FontWeight.SemiBold);
|
||||||
|
addSetWeightStep(FontWeight.Bold);
|
||||||
|
|
||||||
|
void addSetWeightStep(FontWeight newWeight) => AddStep($"set weight {newWeight}", () => weight.Value = newWeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,7 @@
|
|||||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
||||||
<PackageReference Include="DeepEqual" Version="2.0.0" />
|
<PackageReference Include="DeepEqual" Version="2.0.0" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
|
||||||
|
<PackageReference Include="Nito.AsyncEx" Version="5.1.2" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
|
||||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||||
|
@ -29,12 +29,13 @@ namespace osu.Game.Beatmaps
|
|||||||
/// Handles general operations related to global beatmap management.
|
/// Handles general operations related to global beatmap management.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ExcludeFromDynamicCompile]
|
[ExcludeFromDynamicCompile]
|
||||||
public class BeatmapManager : IModelDownloader<BeatmapSetInfo>, IModelManager<BeatmapSetInfo>, IModelFileManager<BeatmapSetInfo, BeatmapSetFileInfo>, ICanAcceptFiles, IWorkingBeatmapCache
|
public class BeatmapManager : IModelDownloader<BeatmapSetInfo>, IModelManager<BeatmapSetInfo>, IModelFileManager<BeatmapSetInfo, BeatmapSetFileInfo>, ICanAcceptFiles, IWorkingBeatmapCache, IDisposable
|
||||||
{
|
{
|
||||||
private readonly BeatmapModelManager beatmapModelManager;
|
private readonly BeatmapModelManager beatmapModelManager;
|
||||||
private readonly BeatmapModelDownloader beatmapModelDownloader;
|
private readonly BeatmapModelDownloader beatmapModelDownloader;
|
||||||
|
|
||||||
private readonly WorkingBeatmapCache workingBeatmapCache;
|
private readonly WorkingBeatmapCache workingBeatmapCache;
|
||||||
|
private readonly BeatmapOnlineLookupQueue onlineBetamapLookupQueue;
|
||||||
|
|
||||||
public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore<byte[]> resources, GameHost host = null,
|
public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore<byte[]> resources, GameHost host = null,
|
||||||
WorkingBeatmap defaultBeatmap = null, bool performOnlineLookups = false)
|
WorkingBeatmap defaultBeatmap = null, bool performOnlineLookups = false)
|
||||||
@ -47,8 +48,8 @@ namespace osu.Game.Beatmaps
|
|||||||
|
|
||||||
if (performOnlineLookups)
|
if (performOnlineLookups)
|
||||||
{
|
{
|
||||||
var onlineBeatmapLookupCache = new BeatmapOnlineLookupQueue(api, storage);
|
onlineBetamapLookupQueue = new BeatmapOnlineLookupQueue(api, storage);
|
||||||
beatmapModelManager.PopulateOnlineInformation = onlineBeatmapLookupCache.UpdateAsync;
|
beatmapModelManager.OnlineLookupQueue = onlineBetamapLookupQueue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -324,5 +325,14 @@ namespace osu.Game.Beatmaps
|
|||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region Implementation of IDisposable
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
onlineBetamapLookupQueue?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,10 +47,9 @@ namespace osu.Game.Beatmaps
|
|||||||
public IBindable<WeakReference<BeatmapInfo>> BeatmapRestored => beatmapRestored;
|
public IBindable<WeakReference<BeatmapInfo>> BeatmapRestored => beatmapRestored;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A function which populates online information during the import process.
|
/// An online lookup queue component which handles populating online beatmap metadata.
|
||||||
/// It is run as the final step of import.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Func<BeatmapSetInfo, CancellationToken, Task> PopulateOnlineInformation;
|
public BeatmapOnlineLookupQueue OnlineLookupQueue { private get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The game working beatmap cache, used to invalidate entries on changes.
|
/// The game working beatmap cache, used to invalidate entries on changes.
|
||||||
@ -102,8 +101,8 @@ namespace osu.Game.Beatmaps
|
|||||||
|
|
||||||
bool hadOnlineBeatmapIDs = beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0);
|
bool hadOnlineBeatmapIDs = beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0);
|
||||||
|
|
||||||
if (PopulateOnlineInformation != null)
|
if (OnlineLookupQueue != null)
|
||||||
await PopulateOnlineInformation(beatmapSet, cancellationToken).ConfigureAwait(false);
|
await OnlineLookupQueue.UpdateAsync(beatmapSet, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
// ensure at least one beatmap was able to retrieve or keep an online ID, else drop the set ID.
|
// ensure at least one beatmap was able to retrieve or keep an online ID, else drop the set ID.
|
||||||
if (hadOnlineBeatmapIDs && !beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0))
|
if (hadOnlineBeatmapIDs && !beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0))
|
||||||
|
@ -25,7 +25,7 @@ namespace osu.Game.Beatmaps
|
|||||||
/// A component which handles population of online IDs for beatmaps using a two part lookup procedure.
|
/// A component which handles population of online IDs for beatmaps using a two part lookup procedure.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// On creating the component, a copy of a database containing metadata for a large subset of beatmaps (stored to <see cref="cache_database_name"/>).
|
/// On creating the component, a copy of a database containing metadata for a large subset of beatmaps (stored to <see cref="cache_database_name"/>) will be downloaded if not already present locally.
|
||||||
/// This will always be checked before doing a second online query to get required metadata.
|
/// This will always be checked before doing a second online query to get required metadata.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[ExcludeFromDynamicCompile]
|
[ExcludeFromDynamicCompile]
|
||||||
@ -211,7 +211,7 @@ namespace osu.Game.Beatmaps
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void logForModel(BeatmapSetInfo set, string message) =>
|
private void logForModel(BeatmapSetInfo set, string message) =>
|
||||||
ArchiveModelManager<BeatmapSetInfo, BeatmapSetFileInfo>.LogForModel(set, $"{nameof(BeatmapOnlineLookupQueue)}] {message}");
|
ArchiveModelManager<BeatmapSetInfo, BeatmapSetFileInfo>.LogForModel(set, $"[{nameof(BeatmapOnlineLookupQueue)}] {message}");
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
@ -176,8 +176,8 @@ namespace osu.Game.Beatmaps.Formats
|
|||||||
case "L":
|
case "L":
|
||||||
{
|
{
|
||||||
var startTime = Parsing.ParseDouble(split[1]);
|
var startTime = Parsing.ParseDouble(split[1]);
|
||||||
var loopCount = Parsing.ParseInt(split[2]);
|
var repeatCount = Parsing.ParseInt(split[2]);
|
||||||
timelineGroup = storyboardSprite?.AddLoop(startTime, loopCount);
|
timelineGroup = storyboardSprite?.AddLoop(startTime, Math.Max(0, repeatCount - 1));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,20 +9,12 @@ namespace osu.Game.Database
|
|||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The main realm context, bound to the update thread.
|
/// The main realm context, bound to the update thread.
|
||||||
/// If querying from a non-update thread is needed, use <see cref="GetForRead"/> or <see cref="GetForWrite"/> to receive a context instead.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Realm Context { get; }
|
Realm Context { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get a fresh context for read usage.
|
/// Create a new realm context for use on the current thread.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
RealmContextFactory.RealmUsage GetForRead();
|
Realm CreateContext();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Request a context for write usage.
|
|
||||||
/// This method may block if a write is already active on a different thread.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>A usage containing a usable context.</returns>
|
|
||||||
RealmContextFactory.RealmWriteUsage GetForWrite();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Development;
|
using osu.Framework.Development;
|
||||||
@ -10,80 +9,117 @@ using osu.Framework.Graphics;
|
|||||||
using osu.Framework.Logging;
|
using osu.Framework.Logging;
|
||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
using osu.Framework.Statistics;
|
using osu.Framework.Statistics;
|
||||||
using osu.Game.Input.Bindings;
|
|
||||||
using Realms;
|
using Realms;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
namespace osu.Game.Database
|
namespace osu.Game.Database
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A factory which provides both the main (update thread bound) realm context and creates contexts for async usage.
|
||||||
|
/// </summary>
|
||||||
public class RealmContextFactory : Component, IRealmFactory
|
public class RealmContextFactory : Component, IRealmFactory
|
||||||
{
|
{
|
||||||
private readonly Storage storage;
|
private readonly Storage storage;
|
||||||
|
|
||||||
private const string database_name = @"client";
|
/// <summary>
|
||||||
|
/// The filename of this realm.
|
||||||
|
/// </summary>
|
||||||
|
public readonly string Filename;
|
||||||
|
|
||||||
private const int schema_version = 6;
|
private const int schema_version = 6;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Lock object which is held for the duration of a write operation (via <see cref="GetForWrite"/>).
|
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking context creation during blocking periods.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly object writeLock = new object();
|
private readonly SemaphoreSlim contextCreationLock = new SemaphoreSlim(1);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Lock object which is held during <see cref="BlockAllOperations"/> sections.
|
|
||||||
/// </summary>
|
|
||||||
private readonly SemaphoreSlim blockingLock = new SemaphoreSlim(1);
|
|
||||||
|
|
||||||
private static readonly GlobalStatistic<int> reads = GlobalStatistics.Get<int>("Realm", "Get (Read)");
|
|
||||||
private static readonly GlobalStatistic<int> writes = GlobalStatistics.Get<int>("Realm", "Get (Write)");
|
|
||||||
private static readonly GlobalStatistic<int> refreshes = GlobalStatistics.Get<int>("Realm", "Dirty Refreshes");
|
private static readonly GlobalStatistic<int> refreshes = GlobalStatistics.Get<int>("Realm", "Dirty Refreshes");
|
||||||
private static readonly GlobalStatistic<int> contexts_created = GlobalStatistics.Get<int>("Realm", "Contexts (Created)");
|
private static readonly GlobalStatistic<int> contexts_created = GlobalStatistics.Get<int>("Realm", "Contexts (Created)");
|
||||||
private static readonly GlobalStatistic<int> pending_writes = GlobalStatistics.Get<int>("Realm", "Pending writes");
|
|
||||||
private static readonly GlobalStatistic<int> active_usages = GlobalStatistics.Get<int>("Realm", "Active usages");
|
|
||||||
|
|
||||||
private readonly object updateContextLock = new object();
|
private readonly object contextLock = new object();
|
||||||
|
private Realm? context;
|
||||||
private Realm context;
|
|
||||||
|
|
||||||
public Realm Context
|
public Realm Context
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (!ThreadSafety.IsUpdateThread)
|
if (!ThreadSafety.IsUpdateThread)
|
||||||
throw new InvalidOperationException($"Use {nameof(GetForRead)} or {nameof(GetForWrite)} when performing realm operations from a non-update thread");
|
throw new InvalidOperationException($"Use {nameof(CreateContext)} when performing realm operations from a non-update thread");
|
||||||
|
|
||||||
lock (updateContextLock)
|
lock (contextLock)
|
||||||
{
|
{
|
||||||
if (context == null)
|
if (context == null)
|
||||||
{
|
{
|
||||||
context = createContext();
|
context = CreateContext();
|
||||||
Logger.Log($"Opened realm \"{context.Config.DatabasePath}\" at version {context.Config.SchemaVersion}");
|
Logger.Log($"Opened realm \"{context.Config.DatabasePath}\" at version {context.Config.SchemaVersion}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// creating a context will ensure our schema is up-to-date and migrated.
|
// creating a context will ensure our schema is up-to-date and migrated.
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public RealmContextFactory(Storage storage)
|
public RealmContextFactory(Storage storage, string filename)
|
||||||
{
|
{
|
||||||
this.storage = storage;
|
this.storage = storage;
|
||||||
|
|
||||||
|
Filename = filename;
|
||||||
|
|
||||||
|
const string realm_extension = ".realm";
|
||||||
|
|
||||||
|
if (!Filename.EndsWith(realm_extension, StringComparison.Ordinal))
|
||||||
|
Filename += realm_extension;
|
||||||
}
|
}
|
||||||
|
|
||||||
public RealmUsage GetForRead()
|
/// <summary>
|
||||||
|
/// Compact this realm.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public bool Compact() => Realm.Compact(getConfiguration());
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
{
|
{
|
||||||
reads.Value++;
|
base.Update();
|
||||||
return new RealmUsage(createContext());
|
|
||||||
|
lock (contextLock)
|
||||||
|
{
|
||||||
|
if (context?.Refresh() == true)
|
||||||
|
refreshes.Value++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public RealmWriteUsage GetForWrite()
|
public Realm CreateContext()
|
||||||
{
|
{
|
||||||
writes.Value++;
|
if (IsDisposed)
|
||||||
pending_writes.Value++;
|
throw new ObjectDisposedException(nameof(RealmContextFactory));
|
||||||
|
|
||||||
Monitor.Enter(writeLock);
|
try
|
||||||
return new RealmWriteUsage(createContext(), writeComplete);
|
{
|
||||||
|
contextCreationLock.Wait();
|
||||||
|
|
||||||
|
contexts_created.Value++;
|
||||||
|
|
||||||
|
return Realm.GetInstance(getConfiguration());
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
contextCreationLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private RealmConfiguration getConfiguration()
|
||||||
|
{
|
||||||
|
return new RealmConfiguration(storage.GetFullPath(Filename, true))
|
||||||
|
{
|
||||||
|
SchemaVersion = schema_version,
|
||||||
|
MigrationCallback = onMigration,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onMigration(Migration migration, ulong lastSchemaVersion)
|
||||||
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -99,165 +135,63 @@ namespace osu.Game.Database
|
|||||||
if (IsDisposed)
|
if (IsDisposed)
|
||||||
throw new ObjectDisposedException(nameof(RealmContextFactory));
|
throw new ObjectDisposedException(nameof(RealmContextFactory));
|
||||||
|
|
||||||
|
// TODO: this can be added for safety once we figure how to bypass in test
|
||||||
|
// if (!ThreadSafety.IsUpdateThread)
|
||||||
|
// throw new InvalidOperationException($"{nameof(BlockAllOperations)} must be called from the update thread.");
|
||||||
|
|
||||||
Logger.Log(@"Blocking realm operations.", LoggingTarget.Database);
|
Logger.Log(@"Blocking realm operations.", LoggingTarget.Database);
|
||||||
|
|
||||||
blockingLock.Wait();
|
|
||||||
flushContexts();
|
|
||||||
|
|
||||||
return new InvokeOnDisposal<RealmContextFactory>(this, endBlockingSection);
|
|
||||||
|
|
||||||
static void endBlockingSection(RealmContextFactory factory)
|
|
||||||
{
|
|
||||||
factory.blockingLock.Release();
|
|
||||||
Logger.Log(@"Restoring realm operations.", LoggingTarget.Database);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Update()
|
|
||||||
{
|
|
||||||
base.Update();
|
|
||||||
|
|
||||||
lock (updateContextLock)
|
|
||||||
{
|
|
||||||
if (context?.Refresh() == true)
|
|
||||||
refreshes.Value++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Realm createContext()
|
|
||||||
{
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (IsDisposed)
|
contextCreationLock.Wait();
|
||||||
throw new ObjectDisposedException(nameof(RealmContextFactory));
|
|
||||||
|
|
||||||
blockingLock.Wait();
|
lock (contextLock)
|
||||||
|
|
||||||
contexts_created.Value++;
|
|
||||||
|
|
||||||
return Realm.GetInstance(new RealmConfiguration(storage.GetFullPath($"{database_name}.realm", true))
|
|
||||||
{
|
{
|
||||||
SchemaVersion = schema_version,
|
context?.Dispose();
|
||||||
MigrationCallback = onMigration,
|
context = null;
|
||||||
});
|
}
|
||||||
|
|
||||||
|
const int sleep_length = 200;
|
||||||
|
int timeout = 5000;
|
||||||
|
|
||||||
|
// see https://github.com/realm/realm-dotnet/discussions/2657
|
||||||
|
while (!Compact())
|
||||||
|
{
|
||||||
|
Thread.Sleep(sleep_length);
|
||||||
|
timeout -= sleep_length;
|
||||||
|
|
||||||
|
if (timeout < 0)
|
||||||
|
throw new TimeoutException("Took too long to acquire lock");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
finally
|
catch
|
||||||
{
|
{
|
||||||
blockingLock.Release();
|
contextCreationLock.Release();
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private void writeComplete()
|
return new InvokeOnDisposal<RealmContextFactory>(this, factory =>
|
||||||
{
|
|
||||||
Monitor.Exit(writeLock);
|
|
||||||
pending_writes.Value--;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onMigration(Migration migration, ulong lastSchemaVersion)
|
|
||||||
{
|
|
||||||
switch (lastSchemaVersion)
|
|
||||||
{
|
{
|
||||||
case 5:
|
factory.contextCreationLock.Release();
|
||||||
// let's keep things simple. changing the type of the primary key is a bit involved.
|
Logger.Log(@"Restoring realm operations.", LoggingTarget.Database);
|
||||||
migration.NewRealm.RemoveAll<RealmKeyBinding>();
|
});
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void flushContexts()
|
|
||||||
{
|
|
||||||
Logger.Log(@"Flushing realm contexts...", LoggingTarget.Database);
|
|
||||||
Debug.Assert(blockingLock.CurrentCount == 0);
|
|
||||||
|
|
||||||
Realm previousContext;
|
|
||||||
|
|
||||||
lock (updateContextLock)
|
|
||||||
{
|
|
||||||
previousContext = context;
|
|
||||||
context = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// wait for all threaded usages to finish
|
|
||||||
while (active_usages.Value > 0)
|
|
||||||
Thread.Sleep(50);
|
|
||||||
|
|
||||||
previousContext?.Dispose();
|
|
||||||
|
|
||||||
Logger.Log(@"Realm contexts flushed.", LoggingTarget.Database);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
protected override void Dispose(bool isDisposing)
|
||||||
{
|
{
|
||||||
|
lock (contextLock)
|
||||||
|
{
|
||||||
|
context?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
if (!IsDisposed)
|
if (!IsDisposed)
|
||||||
{
|
{
|
||||||
// intentionally block all operations indefinitely. this ensures that nothing can start consuming a new context after disposal.
|
// intentionally block context creation indefinitely. this ensures that nothing can start consuming a new context after disposal.
|
||||||
BlockAllOperations();
|
contextCreationLock.Wait();
|
||||||
blockingLock?.Dispose();
|
contextCreationLock.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
base.Dispose(isDisposing);
|
base.Dispose(isDisposing);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A usage of realm from an arbitrary thread.
|
|
||||||
/// </summary>
|
|
||||||
public class RealmUsage : IDisposable
|
|
||||||
{
|
|
||||||
public readonly Realm Realm;
|
|
||||||
|
|
||||||
internal RealmUsage(Realm context)
|
|
||||||
{
|
|
||||||
active_usages.Value++;
|
|
||||||
Realm = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Disposes this instance, calling the initially captured action.
|
|
||||||
/// </summary>
|
|
||||||
public virtual void Dispose()
|
|
||||||
{
|
|
||||||
Realm?.Dispose();
|
|
||||||
active_usages.Value--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A transaction used for making changes to realm data.
|
|
||||||
/// </summary>
|
|
||||||
public class RealmWriteUsage : RealmUsage
|
|
||||||
{
|
|
||||||
private readonly Action onWriteComplete;
|
|
||||||
private readonly Transaction transaction;
|
|
||||||
|
|
||||||
internal RealmWriteUsage(Realm context, Action onWriteComplete)
|
|
||||||
: base(context)
|
|
||||||
{
|
|
||||||
this.onWriteComplete = onWriteComplete;
|
|
||||||
transaction = Realm.BeginWrite();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Commit all changes made in this transaction.
|
|
||||||
/// </summary>
|
|
||||||
public void Commit() => transaction.Commit();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Revert all changes made in this transaction.
|
|
||||||
/// </summary>
|
|
||||||
public void Rollback() => transaction.Rollback();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Disposes this instance, calling the initially captured action.
|
|
||||||
/// </summary>
|
|
||||||
public override void Dispose()
|
|
||||||
{
|
|
||||||
// rollback if not explicitly committed.
|
|
||||||
transaction?.Dispose();
|
|
||||||
|
|
||||||
base.Dispose();
|
|
||||||
|
|
||||||
onWriteComplete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,51 +1,26 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System;
|
||||||
using AutoMapper;
|
|
||||||
using osu.Game.Input.Bindings;
|
|
||||||
using Realms;
|
using Realms;
|
||||||
|
|
||||||
namespace osu.Game.Database
|
namespace osu.Game.Database
|
||||||
{
|
{
|
||||||
public static class RealmExtensions
|
public static class RealmExtensions
|
||||||
{
|
{
|
||||||
private static readonly IMapper mapper = new MapperConfiguration(c =>
|
public static void Write(this Realm realm, Action<Realm> function)
|
||||||
{
|
{
|
||||||
c.ShouldMapField = fi => false;
|
using var transaction = realm.BeginWrite();
|
||||||
c.ShouldMapProperty = pi => pi.SetMethod != null && pi.SetMethod.IsPublic;
|
function(realm);
|
||||||
|
transaction.Commit();
|
||||||
c.CreateMap<RealmKeyBinding, RealmKeyBinding>();
|
|
||||||
}).CreateMapper();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create a detached copy of the each item in the collection.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="items">A list of managed <see cref="RealmObject"/>s to detach.</param>
|
|
||||||
/// <typeparam name="T">The type of object.</typeparam>
|
|
||||||
/// <returns>A list containing non-managed copies of provided items.</returns>
|
|
||||||
public static List<T> Detach<T>(this IEnumerable<T> items) where T : RealmObject
|
|
||||||
{
|
|
||||||
var list = new List<T>();
|
|
||||||
|
|
||||||
foreach (var obj in items)
|
|
||||||
list.Add(obj.Detach());
|
|
||||||
|
|
||||||
return list;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public static T Write<T>(this Realm realm, Func<Realm, T> function)
|
||||||
/// Create a detached copy of the item.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="item">The managed <see cref="RealmObject"/> to detach.</param>
|
|
||||||
/// <typeparam name="T">The type of object.</typeparam>
|
|
||||||
/// <returns>A non-managed copy of provided item. Will return the provided item if already detached.</returns>
|
|
||||||
public static T Detach<T>(this T item) where T : RealmObject
|
|
||||||
{
|
{
|
||||||
if (!item.IsManaged)
|
using var transaction = realm.BeginWrite();
|
||||||
return item;
|
var result = function(realm);
|
||||||
|
transaction.Commit();
|
||||||
return mapper.Map<T>(item);
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
51
osu.Game/Database/RealmObjectExtensions.cs
Normal file
51
osu.Game/Database/RealmObjectExtensions.cs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
// 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.Collections.Generic;
|
||||||
|
using AutoMapper;
|
||||||
|
using osu.Game.Input.Bindings;
|
||||||
|
using Realms;
|
||||||
|
|
||||||
|
namespace osu.Game.Database
|
||||||
|
{
|
||||||
|
public static class RealmObjectExtensions
|
||||||
|
{
|
||||||
|
private static readonly IMapper mapper = new MapperConfiguration(c =>
|
||||||
|
{
|
||||||
|
c.ShouldMapField = fi => false;
|
||||||
|
c.ShouldMapProperty = pi => pi.SetMethod != null && pi.SetMethod.IsPublic;
|
||||||
|
|
||||||
|
c.CreateMap<RealmKeyBinding, RealmKeyBinding>();
|
||||||
|
}).CreateMapper();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a detached copy of the each item in the collection.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="items">A list of managed <see cref="RealmObject"/>s to detach.</param>
|
||||||
|
/// <typeparam name="T">The type of object.</typeparam>
|
||||||
|
/// <returns>A list containing non-managed copies of provided items.</returns>
|
||||||
|
public static List<T> Detach<T>(this IEnumerable<T> items) where T : RealmObject
|
||||||
|
{
|
||||||
|
var list = new List<T>();
|
||||||
|
|
||||||
|
foreach (var obj in items)
|
||||||
|
list.Add(obj.Detach());
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a detached copy of the item.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="item">The managed <see cref="RealmObject"/> to detach.</param>
|
||||||
|
/// <typeparam name="T">The type of object.</typeparam>
|
||||||
|
/// <returns>A non-managed copy of provided item. Will return the provided item if already detached.</returns>
|
||||||
|
public static T Detach<T>(this T item) where T : RealmObject
|
||||||
|
{
|
||||||
|
if (!item.IsManaged)
|
||||||
|
return item;
|
||||||
|
|
||||||
|
return mapper.Map<T>(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -21,6 +21,8 @@ namespace osu.Game.Graphics
|
|||||||
|
|
||||||
public static FontUsage Torus => GetFont(Typeface.Torus, weight: FontWeight.Regular);
|
public static FontUsage Torus => GetFont(Typeface.Torus, weight: FontWeight.Regular);
|
||||||
|
|
||||||
|
public static FontUsage TorusAlternate => GetFont(Typeface.TorusAlternate, weight: FontWeight.Regular);
|
||||||
|
|
||||||
public static FontUsage Inter => GetFont(Typeface.Inter, weight: FontWeight.Regular);
|
public static FontUsage Inter => GetFont(Typeface.Inter, weight: FontWeight.Regular);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -57,6 +59,9 @@ namespace osu.Game.Graphics
|
|||||||
case Typeface.Torus:
|
case Typeface.Torus:
|
||||||
return "Torus";
|
return "Torus";
|
||||||
|
|
||||||
|
case Typeface.TorusAlternate:
|
||||||
|
return "Torus-Alternate";
|
||||||
|
|
||||||
case Typeface.Inter:
|
case Typeface.Inter:
|
||||||
return "Inter";
|
return "Inter";
|
||||||
}
|
}
|
||||||
@ -113,6 +118,7 @@ namespace osu.Game.Graphics
|
|||||||
{
|
{
|
||||||
Venera,
|
Venera,
|
||||||
Torus,
|
Torus,
|
||||||
|
TorusAlternate,
|
||||||
Inter,
|
Inter,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ using osu.Framework.Input.Bindings;
|
|||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
using osu.Game.Input.Bindings;
|
using osu.Game.Input.Bindings;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
|
using Realms;
|
||||||
|
|
||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
@ -30,9 +31,9 @@ namespace osu.Game.Input
|
|||||||
{
|
{
|
||||||
List<string> combinations = new List<string>();
|
List<string> combinations = new List<string>();
|
||||||
|
|
||||||
using (var context = realmFactory.GetForRead())
|
using (var context = realmFactory.CreateContext())
|
||||||
{
|
{
|
||||||
foreach (var action in context.Realm.All<RealmKeyBinding>().Where(b => b.RulesetID == null && (GlobalAction)b.ActionInt == globalAction))
|
foreach (var action in context.All<RealmKeyBinding>().Where(b => b.RulesetID == null && (GlobalAction)b.ActionInt == globalAction))
|
||||||
{
|
{
|
||||||
string str = action.KeyCombination.ReadableString();
|
string str = action.KeyCombination.ReadableString();
|
||||||
|
|
||||||
@ -52,26 +53,27 @@ namespace osu.Game.Input
|
|||||||
/// <param name="rulesets">The rulesets to populate defaults from.</param>
|
/// <param name="rulesets">The rulesets to populate defaults from.</param>
|
||||||
public void Register(KeyBindingContainer container, IEnumerable<RulesetInfo> rulesets)
|
public void Register(KeyBindingContainer container, IEnumerable<RulesetInfo> rulesets)
|
||||||
{
|
{
|
||||||
using (var usage = realmFactory.GetForWrite())
|
using (var realm = realmFactory.CreateContext())
|
||||||
|
using (var transaction = realm.BeginWrite())
|
||||||
{
|
{
|
||||||
// intentionally flattened to a list rather than querying against the IQueryable, as nullable fields being queried against aren't indexed.
|
// intentionally flattened to a list rather than querying against the IQueryable, as nullable fields being queried against aren't indexed.
|
||||||
// this is much faster as a result.
|
// this is much faster as a result.
|
||||||
var existingBindings = usage.Realm.All<RealmKeyBinding>().ToList();
|
var existingBindings = realm.All<RealmKeyBinding>().ToList();
|
||||||
|
|
||||||
insertDefaults(usage, existingBindings, container.DefaultKeyBindings);
|
insertDefaults(realm, existingBindings, container.DefaultKeyBindings);
|
||||||
|
|
||||||
foreach (var ruleset in rulesets)
|
foreach (var ruleset in rulesets)
|
||||||
{
|
{
|
||||||
var instance = ruleset.CreateInstance();
|
var instance = ruleset.CreateInstance();
|
||||||
foreach (var variant in instance.AvailableVariants)
|
foreach (var variant in instance.AvailableVariants)
|
||||||
insertDefaults(usage, existingBindings, instance.GetDefaultKeyBindings(variant), ruleset.ID, variant);
|
insertDefaults(realm, existingBindings, instance.GetDefaultKeyBindings(variant), ruleset.ID, variant);
|
||||||
}
|
}
|
||||||
|
|
||||||
usage.Commit();
|
transaction.Commit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void insertDefaults(RealmContextFactory.RealmUsage usage, List<RealmKeyBinding> existingBindings, IEnumerable<IKeyBinding> defaults, int? rulesetId = null, int? variant = null)
|
private void insertDefaults(Realm realm, List<RealmKeyBinding> existingBindings, IEnumerable<IKeyBinding> defaults, int? rulesetId = null, int? variant = null)
|
||||||
{
|
{
|
||||||
// compare counts in database vs defaults for each action type.
|
// compare counts in database vs defaults for each action type.
|
||||||
foreach (var defaultsForAction in defaults.GroupBy(k => k.Action))
|
foreach (var defaultsForAction in defaults.GroupBy(k => k.Action))
|
||||||
@ -83,7 +85,7 @@ namespace osu.Game.Input
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
// insert any defaults which are missing.
|
// insert any defaults which are missing.
|
||||||
usage.Realm.Add(defaultsForAction.Skip(existingCount).Select(k => new RealmKeyBinding
|
realm.Add(defaultsForAction.Skip(existingCount).Select(k => new RealmKeyBinding
|
||||||
{
|
{
|
||||||
KeyCombinationString = k.KeyCombination.ToString(),
|
KeyCombinationString = k.KeyCombination.ToString(),
|
||||||
ActionInt = (int)k.Action,
|
ActionInt = (int)k.Action,
|
||||||
|
@ -134,7 +134,7 @@ namespace osu.Game.Online.Spectator
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void BeginPlaying(GameplayBeatmap beatmap, Score score)
|
public void BeginPlaying(GameplayState state, Score score)
|
||||||
{
|
{
|
||||||
Debug.Assert(ThreadSafety.IsUpdateThread);
|
Debug.Assert(ThreadSafety.IsUpdateThread);
|
||||||
|
|
||||||
@ -148,7 +148,7 @@ namespace osu.Game.Online.Spectator
|
|||||||
currentState.RulesetID = score.ScoreInfo.RulesetID;
|
currentState.RulesetID = score.ScoreInfo.RulesetID;
|
||||||
currentState.Mods = score.ScoreInfo.Mods.Select(m => new APIMod(m)).ToArray();
|
currentState.Mods = score.ScoreInfo.Mods.Select(m => new APIMod(m)).ToArray();
|
||||||
|
|
||||||
currentBeatmap = beatmap.PlayableBeatmap;
|
currentBeatmap = state.Beatmap;
|
||||||
currentScore = score;
|
currentScore = score;
|
||||||
|
|
||||||
BeginPlayingInternal(currentState);
|
BeginPlayingInternal(currentState);
|
||||||
|
@ -187,7 +187,7 @@ namespace osu.Game
|
|||||||
|
|
||||||
dependencies.Cache(contextFactory = new DatabaseContextFactory(Storage));
|
dependencies.Cache(contextFactory = new DatabaseContextFactory(Storage));
|
||||||
|
|
||||||
dependencies.Cache(realmFactory = new RealmContextFactory(Storage));
|
dependencies.Cache(realmFactory = new RealmContextFactory(Storage, "client"));
|
||||||
|
|
||||||
updateThreadState = Host.UpdateThread.State.GetBoundCopy();
|
updateThreadState = Host.UpdateThread.State.GetBoundCopy();
|
||||||
updateThreadState.BindValueChanged(updateThreadStateChanged);
|
updateThreadState.BindValueChanged(updateThreadStateChanged);
|
||||||
@ -347,6 +347,11 @@ namespace osu.Game
|
|||||||
AddFont(Resources, @"Fonts/Torus/Torus-SemiBold");
|
AddFont(Resources, @"Fonts/Torus/Torus-SemiBold");
|
||||||
AddFont(Resources, @"Fonts/Torus/Torus-Bold");
|
AddFont(Resources, @"Fonts/Torus/Torus-Bold");
|
||||||
|
|
||||||
|
AddFont(Resources, @"Fonts/Torus-Alternate/Torus-Alternate-Regular");
|
||||||
|
AddFont(Resources, @"Fonts/Torus-Alternate/Torus-Alternate-Light");
|
||||||
|
AddFont(Resources, @"Fonts/Torus-Alternate/Torus-Alternate-SemiBold");
|
||||||
|
AddFont(Resources, @"Fonts/Torus-Alternate/Torus-Alternate-Bold");
|
||||||
|
|
||||||
AddFont(Resources, @"Fonts/Inter/Inter-Regular");
|
AddFont(Resources, @"Fonts/Inter/Inter-Regular");
|
||||||
AddFont(Resources, @"Fonts/Inter/Inter-RegularItalic");
|
AddFont(Resources, @"Fonts/Inter/Inter-RegularItalic");
|
||||||
AddFont(Resources, @"Fonts/Inter/Inter-Light");
|
AddFont(Resources, @"Fonts/Inter/Inter-Light");
|
||||||
@ -448,19 +453,20 @@ namespace osu.Game
|
|||||||
private void migrateDataToRealm()
|
private void migrateDataToRealm()
|
||||||
{
|
{
|
||||||
using (var db = contextFactory.GetForWrite())
|
using (var db = contextFactory.GetForWrite())
|
||||||
using (var usage = realmFactory.GetForWrite())
|
using (var realm = realmFactory.CreateContext())
|
||||||
|
using (var transaction = realm.BeginWrite())
|
||||||
{
|
{
|
||||||
// migrate ruleset settings. can be removed 20220315.
|
// migrate ruleset settings. can be removed 20220315.
|
||||||
var existingSettings = db.Context.DatabasedSetting;
|
var existingSettings = db.Context.DatabasedSetting;
|
||||||
|
|
||||||
// only migrate data if the realm database is empty.
|
// only migrate data if the realm database is empty.
|
||||||
if (!usage.Realm.All<RealmRulesetSetting>().Any())
|
if (!realm.All<RealmRulesetSetting>().Any())
|
||||||
{
|
{
|
||||||
foreach (var dkb in existingSettings)
|
foreach (var dkb in existingSettings)
|
||||||
{
|
{
|
||||||
if (dkb.RulesetID == null) continue;
|
if (dkb.RulesetID == null) continue;
|
||||||
|
|
||||||
usage.Realm.Add(new RealmRulesetSetting
|
realm.Add(new RealmRulesetSetting
|
||||||
{
|
{
|
||||||
Key = dkb.Key,
|
Key = dkb.Key,
|
||||||
Value = dkb.StringValue,
|
Value = dkb.StringValue,
|
||||||
@ -472,7 +478,7 @@ namespace osu.Game
|
|||||||
|
|
||||||
db.Context.RemoveRange(existingSettings);
|
db.Context.RemoveRange(existingSettings);
|
||||||
|
|
||||||
usage.Commit();
|
transaction.Commit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -524,6 +530,7 @@ namespace osu.Game
|
|||||||
base.Dispose(isDisposing);
|
base.Dispose(isDisposing);
|
||||||
|
|
||||||
RulesetStore?.Dispose();
|
RulesetStore?.Dispose();
|
||||||
|
BeatmapManager?.Dispose();
|
||||||
LocalConfig?.Dispose();
|
LocalConfig?.Dispose();
|
||||||
|
|
||||||
contextFactory?.FlushConnections();
|
contextFactory?.FlushConnections();
|
||||||
|
@ -284,6 +284,10 @@ namespace osu.Game.Overlays
|
|||||||
if (currentChannel.Value != e.NewValue)
|
if (currentChannel.Value != e.NewValue)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// check once more to ensure the channel hasn't since been removed from the loaded channels list (may have been left by some automated means).
|
||||||
|
if (loadedChannels.Contains(loaded))
|
||||||
|
return;
|
||||||
|
|
||||||
loading.Hide();
|
loading.Hide();
|
||||||
|
|
||||||
currentChannelContainer.Clear(false);
|
currentChannelContainer.Clear(false);
|
||||||
@ -444,10 +448,9 @@ namespace osu.Game.Overlays
|
|||||||
|
|
||||||
if (loaded != null)
|
if (loaded != null)
|
||||||
{
|
{
|
||||||
loadedChannels.Remove(loaded);
|
|
||||||
|
|
||||||
// Because the container is only cleared in the async load callback of a new channel, it is forcefully cleared
|
// Because the container is only cleared in the async load callback of a new channel, it is forcefully cleared
|
||||||
// to ensure that the previous channel doesn't get updated after it's disposed
|
// to ensure that the previous channel doesn't get updated after it's disposed
|
||||||
|
loadedChannels.Remove(loaded);
|
||||||
currentChannelContainer.Remove(loaded);
|
currentChannelContainer.Remove(loaded);
|
||||||
loaded.Dispose();
|
loaded.Dispose();
|
||||||
}
|
}
|
||||||
|
@ -368,12 +368,10 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
|||||||
|
|
||||||
private void updateStoreFromButton(KeyButton button)
|
private void updateStoreFromButton(KeyButton button)
|
||||||
{
|
{
|
||||||
using (var usage = realmFactory.GetForWrite())
|
using (var realm = realmFactory.CreateContext())
|
||||||
{
|
{
|
||||||
var binding = usage.Realm.Find<RealmKeyBinding>(((IHasGuidPrimaryKey)button.KeyBinding).ID);
|
var binding = realm.Find<RealmKeyBinding>(((IHasGuidPrimaryKey)button.KeyBinding).ID);
|
||||||
binding.KeyCombinationString = button.KeyBinding.KeyCombinationString;
|
realm.Write(() => binding.KeyCombinationString = button.KeyBinding.KeyCombinationString);
|
||||||
|
|
||||||
usage.Commit();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,8 +38,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
|||||||
|
|
||||||
List<RealmKeyBinding> bindings;
|
List<RealmKeyBinding> bindings;
|
||||||
|
|
||||||
using (var usage = realmFactory.GetForRead())
|
using (var realm = realmFactory.CreateContext())
|
||||||
bindings = usage.Realm.All<RealmKeyBinding>().Where(b => b.RulesetID == rulesetId && b.Variant == variant).Detach();
|
bindings = realm.All<RealmKeyBinding>().Where(b => b.RulesetID == rulesetId && b.Variant == variant).Detach();
|
||||||
|
|
||||||
foreach (var defaultGroup in Defaults.GroupBy(d => d.Action))
|
foreach (var defaultGroup in Defaults.GroupBy(d => d.Action))
|
||||||
{
|
{
|
||||||
@ -75,5 +75,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
|||||||
|
|
||||||
Content.CornerRadius = 5;
|
Content.CornerRadius = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Empty FilterTerms so that the ResetButton is visible only when the whole subsection is visible.
|
||||||
|
public override IEnumerable<string> FilterTerms => Enumerable.Empty<string>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.UI
|
|||||||
private SpectatorClient spectatorClient { get; set; }
|
private SpectatorClient spectatorClient { get; set; }
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private GameplayBeatmap gameplayBeatmap { get; set; }
|
private GameplayState gameplayState { get; set; }
|
||||||
|
|
||||||
protected ReplayRecorder(Score target)
|
protected ReplayRecorder(Score target)
|
||||||
{
|
{
|
||||||
@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.UI
|
|||||||
|
|
||||||
inputManager = GetContainingInputManager();
|
inputManager = GetContainingInputManager();
|
||||||
|
|
||||||
spectatorClient?.BeginPlaying(gameplayBeatmap, target);
|
spectatorClient?.BeginPlaying(gameplayState, target);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
protected override void Dispose(bool isDisposing)
|
||||||
|
@ -72,21 +72,21 @@ namespace osu.Game.Screens.OnlinePlay
|
|||||||
{
|
{
|
||||||
Anchor = Anchor.CentreLeft,
|
Anchor = Anchor.CentreLeft,
|
||||||
Origin = Anchor.CentreLeft,
|
Origin = Anchor.CentreLeft,
|
||||||
Font = OsuFont.GetFont(size: 24),
|
Font = OsuFont.TorusAlternate.With(size: 24),
|
||||||
Text = mainTitle
|
Text = mainTitle
|
||||||
},
|
},
|
||||||
dot = new OsuSpriteText
|
dot = new OsuSpriteText
|
||||||
{
|
{
|
||||||
Anchor = Anchor.CentreLeft,
|
Anchor = Anchor.CentreLeft,
|
||||||
Origin = Anchor.CentreLeft,
|
Origin = Anchor.CentreLeft,
|
||||||
Font = OsuFont.GetFont(size: 24),
|
Font = OsuFont.TorusAlternate.With(size: 24),
|
||||||
Text = "·"
|
Text = "·"
|
||||||
},
|
},
|
||||||
pageTitle = new OsuSpriteText
|
pageTitle = new OsuSpriteText
|
||||||
{
|
{
|
||||||
Anchor = Anchor.CentreLeft,
|
Anchor = Anchor.CentreLeft,
|
||||||
Origin = Anchor.CentreLeft,
|
Origin = Anchor.CentreLeft,
|
||||||
Font = OsuFont.GetFont(size: 24),
|
Font = OsuFont.TorusAlternate.With(size: 24),
|
||||||
Text = "Lounge"
|
Text = "Lounge"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -213,8 +213,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void StartGameplay(int userId, GameplayState gameplayState)
|
protected override void StartGameplay(int userId, SpectatorGameplayState spectatorGameplayState)
|
||||||
=> instances.Single(i => i.UserId == userId).LoadScore(gameplayState.Score);
|
=> instances.Single(i => i.UserId == userId).LoadScore(spectatorGameplayState.Score);
|
||||||
|
|
||||||
protected override void EndGameplay(int userId)
|
protected override void EndGameplay(int userId)
|
||||||
{
|
{
|
||||||
|
@ -1,56 +0,0 @@
|
|||||||
// 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.Collections.Generic;
|
|
||||||
using osu.Framework.Bindables;
|
|
||||||
using osu.Framework.Graphics;
|
|
||||||
using osu.Game.Beatmaps;
|
|
||||||
using osu.Game.Beatmaps.ControlPoints;
|
|
||||||
using osu.Game.Beatmaps.Timing;
|
|
||||||
using osu.Game.Rulesets.Judgements;
|
|
||||||
using osu.Game.Rulesets.Objects;
|
|
||||||
|
|
||||||
namespace osu.Game.Screens.Play
|
|
||||||
{
|
|
||||||
public class GameplayBeatmap : Component, IBeatmap
|
|
||||||
{
|
|
||||||
public readonly IBeatmap PlayableBeatmap;
|
|
||||||
|
|
||||||
public GameplayBeatmap(IBeatmap playableBeatmap)
|
|
||||||
{
|
|
||||||
PlayableBeatmap = playableBeatmap;
|
|
||||||
}
|
|
||||||
|
|
||||||
public BeatmapInfo BeatmapInfo
|
|
||||||
{
|
|
||||||
get => PlayableBeatmap.BeatmapInfo;
|
|
||||||
set => PlayableBeatmap.BeatmapInfo = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public BeatmapMetadata Metadata => PlayableBeatmap.Metadata;
|
|
||||||
|
|
||||||
public ControlPointInfo ControlPointInfo
|
|
||||||
{
|
|
||||||
get => PlayableBeatmap.ControlPointInfo;
|
|
||||||
set => PlayableBeatmap.ControlPointInfo = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<BreakPeriod> Breaks => PlayableBeatmap.Breaks;
|
|
||||||
|
|
||||||
public double TotalBreakTime => PlayableBeatmap.TotalBreakTime;
|
|
||||||
|
|
||||||
public IReadOnlyList<HitObject> HitObjects => PlayableBeatmap.HitObjects;
|
|
||||||
|
|
||||||
public IEnumerable<BeatmapStatistic> GetStatistics() => PlayableBeatmap.GetStatistics();
|
|
||||||
|
|
||||||
public double GetMostCommonBeatLength() => PlayableBeatmap.GetMostCommonBeatLength();
|
|
||||||
|
|
||||||
public IBeatmap Clone() => PlayableBeatmap.Clone();
|
|
||||||
|
|
||||||
private readonly Bindable<JudgementResult> lastJudgementResult = new Bindable<JudgementResult>();
|
|
||||||
|
|
||||||
public IBindable<JudgementResult> LastJudgementResult => lastJudgementResult;
|
|
||||||
|
|
||||||
public void ApplyResult(JudgementResult result) => lastJudgementResult.Value = result;
|
|
||||||
}
|
|
||||||
}
|
|
55
osu.Game/Screens/Play/GameplayState.cs
Normal file
55
osu.Game/Screens/Play/GameplayState.cs
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
// 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.Collections.Generic;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
|
using osu.Game.Rulesets.Judgements;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.Play
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The state of an active gameplay session, generally constructed and exposed by <see cref="Player"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class GameplayState
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The final post-convert post-mod-application beatmap.
|
||||||
|
/// </summary>
|
||||||
|
public readonly IBeatmap Beatmap;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The ruleset used in gameplay.
|
||||||
|
/// </summary>
|
||||||
|
public readonly Ruleset Ruleset;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The mods applied to the gameplay.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<Mod> Mods;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A bindable tracking the last judgement result applied to any hit object.
|
||||||
|
/// </summary>
|
||||||
|
public IBindable<JudgementResult> LastJudgementResult => lastJudgementResult;
|
||||||
|
|
||||||
|
private readonly Bindable<JudgementResult> lastJudgementResult = new Bindable<JudgementResult>();
|
||||||
|
|
||||||
|
public GameplayState(IBeatmap beatmap, Ruleset ruleset, IReadOnlyList<Mod> mods)
|
||||||
|
{
|
||||||
|
Beatmap = beatmap;
|
||||||
|
Ruleset = ruleset;
|
||||||
|
Mods = mods;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies the score change of a <see cref="JudgementResult"/> to this <see cref="GameplayState"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="result">The <see cref="JudgementResult"/> to apply.</param>
|
||||||
|
public void ApplyResult(JudgementResult result) => lastJudgementResult.Value = result;
|
||||||
|
}
|
||||||
|
}
|
@ -93,9 +93,9 @@ namespace osu.Game.Screens.Play
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private SpectatorClient spectatorClient { get; set; }
|
private SpectatorClient spectatorClient { get; set; }
|
||||||
|
|
||||||
protected Ruleset GameplayRuleset { get; private set; }
|
public GameplayState GameplayState { get; private set; }
|
||||||
|
|
||||||
protected GameplayBeatmap GameplayBeatmap { get; private set; }
|
private Ruleset ruleset;
|
||||||
|
|
||||||
private Sample sampleRestart;
|
private Sample sampleRestart;
|
||||||
|
|
||||||
@ -165,7 +165,7 @@ namespace osu.Game.Screens.Play
|
|||||||
|
|
||||||
// ensure the score is in a consistent state with the current player.
|
// ensure the score is in a consistent state with the current player.
|
||||||
Score.ScoreInfo.Beatmap = Beatmap.Value.BeatmapInfo;
|
Score.ScoreInfo.Beatmap = Beatmap.Value.BeatmapInfo;
|
||||||
Score.ScoreInfo.Ruleset = GameplayRuleset.RulesetInfo;
|
Score.ScoreInfo.Ruleset = ruleset.RulesetInfo;
|
||||||
Score.ScoreInfo.Mods = Mods.Value.ToArray();
|
Score.ScoreInfo.Mods = Mods.Value.ToArray();
|
||||||
|
|
||||||
PrepareReplay();
|
PrepareReplay();
|
||||||
@ -206,16 +206,16 @@ namespace osu.Game.Screens.Play
|
|||||||
if (game is OsuGame osuGame)
|
if (game is OsuGame osuGame)
|
||||||
LocalUserPlaying.BindTo(osuGame.LocalUserPlaying);
|
LocalUserPlaying.BindTo(osuGame.LocalUserPlaying);
|
||||||
|
|
||||||
DrawableRuleset = GameplayRuleset.CreateDrawableRulesetWith(playableBeatmap, Mods.Value);
|
DrawableRuleset = ruleset.CreateDrawableRulesetWith(playableBeatmap, Mods.Value);
|
||||||
dependencies.CacheAs(DrawableRuleset);
|
dependencies.CacheAs(DrawableRuleset);
|
||||||
|
|
||||||
ScoreProcessor = GameplayRuleset.CreateScoreProcessor();
|
ScoreProcessor = ruleset.CreateScoreProcessor();
|
||||||
ScoreProcessor.ApplyBeatmap(playableBeatmap);
|
ScoreProcessor.ApplyBeatmap(playableBeatmap);
|
||||||
ScoreProcessor.Mods.BindTo(Mods);
|
ScoreProcessor.Mods.BindTo(Mods);
|
||||||
|
|
||||||
dependencies.CacheAs(ScoreProcessor);
|
dependencies.CacheAs(ScoreProcessor);
|
||||||
|
|
||||||
HealthProcessor = GameplayRuleset.CreateHealthProcessor(playableBeatmap.HitObjects[0].StartTime);
|
HealthProcessor = ruleset.CreateHealthProcessor(playableBeatmap.HitObjects[0].StartTime);
|
||||||
HealthProcessor.ApplyBeatmap(playableBeatmap);
|
HealthProcessor.ApplyBeatmap(playableBeatmap);
|
||||||
|
|
||||||
dependencies.CacheAs(HealthProcessor);
|
dependencies.CacheAs(HealthProcessor);
|
||||||
@ -225,12 +225,11 @@ namespace osu.Game.Screens.Play
|
|||||||
|
|
||||||
InternalChild = GameplayClockContainer = CreateGameplayClockContainer(Beatmap.Value, DrawableRuleset.GameplayStartTime);
|
InternalChild = GameplayClockContainer = CreateGameplayClockContainer(Beatmap.Value, DrawableRuleset.GameplayStartTime);
|
||||||
|
|
||||||
AddInternal(GameplayBeatmap = new GameplayBeatmap(playableBeatmap));
|
dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, Mods.Value));
|
||||||
|
|
||||||
AddInternal(screenSuspension = new ScreenSuspensionHandler(GameplayClockContainer));
|
AddInternal(screenSuspension = new ScreenSuspensionHandler(GameplayClockContainer));
|
||||||
|
|
||||||
dependencies.CacheAs(GameplayBeatmap);
|
var rulesetSkinProvider = new RulesetSkinProvidingContainer(ruleset, playableBeatmap, Beatmap.Value.Skin);
|
||||||
|
|
||||||
var rulesetSkinProvider = new RulesetSkinProvidingContainer(GameplayRuleset, playableBeatmap, Beatmap.Value.Skin);
|
|
||||||
|
|
||||||
// load the skinning hierarchy first.
|
// load the skinning hierarchy first.
|
||||||
// this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources.
|
// this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources.
|
||||||
@ -280,7 +279,7 @@ namespace osu.Game.Screens.Play
|
|||||||
{
|
{
|
||||||
HealthProcessor.ApplyResult(r);
|
HealthProcessor.ApplyResult(r);
|
||||||
ScoreProcessor.ApplyResult(r);
|
ScoreProcessor.ApplyResult(r);
|
||||||
GameplayBeatmap.ApplyResult(r);
|
GameplayState.ApplyResult(r);
|
||||||
};
|
};
|
||||||
|
|
||||||
DrawableRuleset.RevertResult += r =>
|
DrawableRuleset.RevertResult += r =>
|
||||||
@ -478,17 +477,17 @@ namespace osu.Game.Screens.Play
|
|||||||
throw new InvalidOperationException("Beatmap was not loaded");
|
throw new InvalidOperationException("Beatmap was not loaded");
|
||||||
|
|
||||||
var rulesetInfo = Ruleset.Value ?? Beatmap.Value.BeatmapInfo.Ruleset;
|
var rulesetInfo = Ruleset.Value ?? Beatmap.Value.BeatmapInfo.Ruleset;
|
||||||
GameplayRuleset = rulesetInfo.CreateInstance();
|
ruleset = rulesetInfo.CreateInstance();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
playable = Beatmap.Value.GetPlayableBeatmap(GameplayRuleset.RulesetInfo, Mods.Value);
|
playable = Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo, Mods.Value);
|
||||||
}
|
}
|
||||||
catch (BeatmapInvalidForRulesetException)
|
catch (BeatmapInvalidForRulesetException)
|
||||||
{
|
{
|
||||||
// A playable beatmap may not be creatable with the user's preferred ruleset, so try using the beatmap's default ruleset
|
// A playable beatmap may not be creatable with the user's preferred ruleset, so try using the beatmap's default ruleset
|
||||||
rulesetInfo = Beatmap.Value.BeatmapInfo.Ruleset;
|
rulesetInfo = Beatmap.Value.BeatmapInfo.Ruleset;
|
||||||
GameplayRuleset = rulesetInfo.CreateInstance();
|
ruleset = rulesetInfo.CreateInstance();
|
||||||
|
|
||||||
playable = Beatmap.Value.GetPlayableBeatmap(rulesetInfo, Mods.Value);
|
playable = Beatmap.Value.GetPlayableBeatmap(rulesetInfo, Mods.Value);
|
||||||
}
|
}
|
||||||
@ -1010,7 +1009,7 @@ namespace osu.Game.Screens.Play
|
|||||||
|
|
||||||
using (var stream = new MemoryStream())
|
using (var stream = new MemoryStream())
|
||||||
{
|
{
|
||||||
new LegacyScoreEncoder(score, GameplayBeatmap.PlayableBeatmap).Encode(stream);
|
new LegacyScoreEncoder(score, GameplayState.Beatmap).Encode(stream);
|
||||||
replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr");
|
replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ namespace osu.Game.Screens.Play
|
|||||||
DrawableRuleset?.SetReplayScore(Score);
|
DrawableRuleset?.SetReplayScore(Score);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Score CreateScore() => createScore(GameplayBeatmap.PlayableBeatmap, Mods.Value);
|
protected override Score CreateScore() => createScore(GameplayState.Beatmap, Mods.Value);
|
||||||
|
|
||||||
// Don't re-import replay scores as they're already present in the database.
|
// Don't re-import replay scores as they're already present in the database.
|
||||||
protected override Task ImportScore(Score score) => Task.CompletedTask;
|
protected override Task ImportScore(Score score) => Task.CompletedTask;
|
||||||
@ -78,7 +78,7 @@ namespace osu.Game.Screens.Play
|
|||||||
|
|
||||||
void keyboardSeek(int direction)
|
void keyboardSeek(int direction)
|
||||||
{
|
{
|
||||||
double target = Math.Clamp(GameplayClockContainer.CurrentTime + direction * keyboard_seek_amount, 0, GameplayBeatmap.HitObjects.Last().GetEndTime());
|
double target = Math.Clamp(GameplayClockContainer.CurrentTime + direction * keyboard_seek_amount, 0, GameplayState.Beatmap.HitObjects.Last().GetEndTime());
|
||||||
|
|
||||||
Seek(target);
|
Seek(target);
|
||||||
}
|
}
|
||||||
|
@ -56,7 +56,7 @@ namespace osu.Game.Screens.Play
|
|||||||
/// The player's immediate online gameplay state.
|
/// The player's immediate online gameplay state.
|
||||||
/// This doesn't always reflect the gameplay state being watched.
|
/// This doesn't always reflect the gameplay state being watched.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private GameplayState immediateGameplayState;
|
private SpectatorGameplayState immediateSpectatorGameplayState;
|
||||||
|
|
||||||
private GetBeatmapSetRequest onlineBeatmapRequest;
|
private GetBeatmapSetRequest onlineBeatmapRequest;
|
||||||
|
|
||||||
@ -146,7 +146,7 @@ namespace osu.Game.Screens.Play
|
|||||||
Width = 250,
|
Width = 250,
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Action = () => scheduleStart(immediateGameplayState),
|
Action = () => scheduleStart(immediateSpectatorGameplayState),
|
||||||
Enabled = { Value = false }
|
Enabled = { Value = false }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -167,18 +167,18 @@ namespace osu.Game.Screens.Play
|
|||||||
showBeatmapPanel(spectatorState);
|
showBeatmapPanel(spectatorState);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void StartGameplay(int userId, GameplayState gameplayState)
|
protected override void StartGameplay(int userId, SpectatorGameplayState spectatorGameplayState)
|
||||||
{
|
{
|
||||||
immediateGameplayState = gameplayState;
|
immediateSpectatorGameplayState = spectatorGameplayState;
|
||||||
watchButton.Enabled.Value = true;
|
watchButton.Enabled.Value = true;
|
||||||
|
|
||||||
scheduleStart(gameplayState);
|
scheduleStart(spectatorGameplayState);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void EndGameplay(int userId)
|
protected override void EndGameplay(int userId)
|
||||||
{
|
{
|
||||||
scheduledStart?.Cancel();
|
scheduledStart?.Cancel();
|
||||||
immediateGameplayState = null;
|
immediateSpectatorGameplayState = null;
|
||||||
watchButton.Enabled.Value = false;
|
watchButton.Enabled.Value = false;
|
||||||
|
|
||||||
clearDisplay();
|
clearDisplay();
|
||||||
@ -194,7 +194,7 @@ namespace osu.Game.Screens.Play
|
|||||||
|
|
||||||
private ScheduledDelegate scheduledStart;
|
private ScheduledDelegate scheduledStart;
|
||||||
|
|
||||||
private void scheduleStart(GameplayState gameplayState)
|
private void scheduleStart(SpectatorGameplayState spectatorGameplayState)
|
||||||
{
|
{
|
||||||
// This function may be called multiple times in quick succession once the screen becomes current again.
|
// This function may be called multiple times in quick succession once the screen becomes current again.
|
||||||
scheduledStart?.Cancel();
|
scheduledStart?.Cancel();
|
||||||
@ -203,15 +203,15 @@ namespace osu.Game.Screens.Play
|
|||||||
if (this.IsCurrentScreen())
|
if (this.IsCurrentScreen())
|
||||||
start();
|
start();
|
||||||
else
|
else
|
||||||
scheduleStart(gameplayState);
|
scheduleStart(spectatorGameplayState);
|
||||||
});
|
});
|
||||||
|
|
||||||
void start()
|
void start()
|
||||||
{
|
{
|
||||||
Beatmap.Value = gameplayState.Beatmap;
|
Beatmap.Value = spectatorGameplayState.Beatmap;
|
||||||
Ruleset.Value = gameplayState.Ruleset.RulesetInfo;
|
Ruleset.Value = spectatorGameplayState.Ruleset.RulesetInfo;
|
||||||
|
|
||||||
this.Push(new SpectatorPlayerLoader(gameplayState.Score, () => new SoloSpectatorPlayer(gameplayState.Score)));
|
this.Push(new SpectatorPlayerLoader(spectatorGameplayState.Score, () => new SoloSpectatorPlayer(spectatorGameplayState.Score)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,8 +66,8 @@ namespace osu.Game.Screens.Play
|
|||||||
|
|
||||||
foreach (var frame in bundle.Frames)
|
foreach (var frame in bundle.Frames)
|
||||||
{
|
{
|
||||||
IConvertibleReplayFrame convertibleFrame = GameplayRuleset.CreateConvertibleReplayFrame();
|
IConvertibleReplayFrame convertibleFrame = GameplayState.Ruleset.CreateConvertibleReplayFrame();
|
||||||
convertibleFrame.FromLegacy(frame, GameplayBeatmap.PlayableBeatmap);
|
convertibleFrame.FromLegacy(frame, GameplayState.Beatmap);
|
||||||
|
|
||||||
var convertedFrame = (ReplayFrame)convertibleFrame;
|
var convertedFrame = (ReplayFrame)convertibleFrame;
|
||||||
convertedFrame.Time = frame.Time;
|
convertedFrame.Time = frame.Time;
|
||||||
|
@ -8,9 +8,9 @@ using osu.Game.Scoring;
|
|||||||
namespace osu.Game.Screens.Spectate
|
namespace osu.Game.Screens.Spectate
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The gameplay state of a spectated user. This class is immutable.
|
/// An immutable spectator gameplay state.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class GameplayState
|
public class SpectatorGameplayState
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The score which the user is playing.
|
/// The score which the user is playing.
|
||||||
@ -27,7 +27,7 @@ namespace osu.Game.Screens.Spectate
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly WorkingBeatmap Beatmap;
|
public readonly WorkingBeatmap Beatmap;
|
||||||
|
|
||||||
public GameplayState(Score score, Ruleset ruleset, WorkingBeatmap beatmap)
|
public SpectatorGameplayState(Score score, Ruleset ruleset, WorkingBeatmap beatmap)
|
||||||
{
|
{
|
||||||
Score = score;
|
Score = score;
|
||||||
Ruleset = ruleset;
|
Ruleset = ruleset;
|
@ -43,7 +43,7 @@ namespace osu.Game.Screens.Spectate
|
|||||||
private readonly IBindableDictionary<int, SpectatorState> playingUserStates = new BindableDictionary<int, SpectatorState>();
|
private readonly IBindableDictionary<int, SpectatorState> playingUserStates = new BindableDictionary<int, SpectatorState>();
|
||||||
|
|
||||||
private readonly Dictionary<int, User> userMap = new Dictionary<int, User>();
|
private readonly Dictionary<int, User> userMap = new Dictionary<int, User>();
|
||||||
private readonly Dictionary<int, GameplayState> gameplayStates = new Dictionary<int, GameplayState>();
|
private readonly Dictionary<int, SpectatorGameplayState> gameplayStates = new Dictionary<int, SpectatorGameplayState>();
|
||||||
|
|
||||||
private IBindable<WeakReference<BeatmapSetInfo>> managerUpdated;
|
private IBindable<WeakReference<BeatmapSetInfo>> managerUpdated;
|
||||||
|
|
||||||
@ -173,7 +173,7 @@ namespace osu.Game.Screens.Spectate
|
|||||||
Replay = new Replay { HasReceivedAllFrames = false },
|
Replay = new Replay { HasReceivedAllFrames = false },
|
||||||
};
|
};
|
||||||
|
|
||||||
var gameplayState = new GameplayState(score, resolvedRuleset, beatmaps.GetWorkingBeatmap(resolvedBeatmap));
|
var gameplayState = new SpectatorGameplayState(score, resolvedRuleset, beatmaps.GetWorkingBeatmap(resolvedBeatmap));
|
||||||
|
|
||||||
gameplayStates[userId] = gameplayState;
|
gameplayStates[userId] = gameplayState;
|
||||||
Schedule(() => StartGameplay(userId, gameplayState));
|
Schedule(() => StartGameplay(userId, gameplayState));
|
||||||
@ -190,8 +190,8 @@ namespace osu.Game.Screens.Spectate
|
|||||||
/// Starts gameplay for a user.
|
/// Starts gameplay for a user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="userId">The user to start gameplay for.</param>
|
/// <param name="userId">The user to start gameplay for.</param>
|
||||||
/// <param name="gameplayState">The gameplay state.</param>
|
/// <param name="spectatorGameplayState">The gameplay state.</param>
|
||||||
protected abstract void StartGameplay(int userId, [NotNull] GameplayState gameplayState);
|
protected abstract void StartGameplay(int userId, [NotNull] SpectatorGameplayState spectatorGameplayState);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ends gameplay for a user.
|
/// Ends gameplay for a user.
|
||||||
|
@ -11,6 +11,7 @@ using osu.Framework.Bindables;
|
|||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.OpenGL.Textures;
|
using osu.Framework.Graphics.OpenGL.Textures;
|
||||||
using osu.Framework.Graphics.Textures;
|
using osu.Framework.Graphics.Textures;
|
||||||
|
using osu.Framework.Logging;
|
||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
using osu.Game.IO;
|
using osu.Game.IO;
|
||||||
using osu.Game.Screens.Play.HUD;
|
using osu.Game.Screens.Play.HUD;
|
||||||
@ -55,13 +56,20 @@ namespace osu.Game.Skinning
|
|||||||
if (bytes == null)
|
if (bytes == null)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
string jsonContent = Encoding.UTF8.GetString(bytes);
|
try
|
||||||
var deserializedContent = JsonConvert.DeserializeObject<IEnumerable<SkinnableInfo>>(jsonContent);
|
{
|
||||||
|
string jsonContent = Encoding.UTF8.GetString(bytes);
|
||||||
|
var deserializedContent = JsonConvert.DeserializeObject<IEnumerable<SkinnableInfo>>(jsonContent);
|
||||||
|
|
||||||
if (deserializedContent == null)
|
if (deserializedContent == null)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
DrawableComponentInfo[skinnableTarget] = deserializedContent.ToArray();
|
DrawableComponentInfo[skinnableTarget] = deserializedContent.ToArray();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.Error(ex, "Failed to load skin configuration.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace osu.Game.Storyboards
|
namespace osu.Game.Storyboards
|
||||||
@ -8,20 +9,31 @@ namespace osu.Game.Storyboards
|
|||||||
public class CommandLoop : CommandTimelineGroup
|
public class CommandLoop : CommandTimelineGroup
|
||||||
{
|
{
|
||||||
public double LoopStartTime;
|
public double LoopStartTime;
|
||||||
public int LoopCount;
|
|
||||||
|
/// <summary>
|
||||||
|
/// The total number of times this loop is played back. Always greater than zero.
|
||||||
|
/// </summary>
|
||||||
|
public readonly int TotalIterations;
|
||||||
|
|
||||||
public override double StartTime => LoopStartTime + CommandsStartTime;
|
public override double StartTime => LoopStartTime + CommandsStartTime;
|
||||||
public override double EndTime => StartTime + CommandsDuration * LoopCount;
|
public override double EndTime => StartTime + CommandsDuration * TotalIterations;
|
||||||
|
|
||||||
public CommandLoop(double startTime, int loopCount)
|
/// <summary>
|
||||||
|
/// Construct a new command loop.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="startTime">The start time of the loop.</param>
|
||||||
|
/// <param name="repeatCount">The number of times the loop should repeat. Should be greater than zero. Zero means a single playback.</param>
|
||||||
|
public CommandLoop(double startTime, int repeatCount)
|
||||||
{
|
{
|
||||||
|
if (repeatCount < 0) throw new ArgumentException("Repeat count must be zero or above.", nameof(repeatCount));
|
||||||
|
|
||||||
LoopStartTime = startTime;
|
LoopStartTime = startTime;
|
||||||
LoopCount = loopCount;
|
TotalIterations = repeatCount + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override IEnumerable<CommandTimeline<T>.TypedCommand> GetCommands<T>(CommandTimelineSelector<T> timelineSelector, double offset = 0)
|
public override IEnumerable<CommandTimeline<T>.TypedCommand> GetCommands<T>(CommandTimelineSelector<T> timelineSelector, double offset = 0)
|
||||||
{
|
{
|
||||||
for (var loop = 0; loop < LoopCount; loop++)
|
for (var loop = 0; loop < TotalIterations; loop++)
|
||||||
{
|
{
|
||||||
var loopOffset = LoopStartTime + loop * CommandsDuration;
|
var loopOffset = LoopStartTime + loop * CommandsDuration;
|
||||||
foreach (var command in base.GetCommands(timelineSelector, offset + loopOffset))
|
foreach (var command in base.GetCommands(timelineSelector, offset + loopOffset))
|
||||||
@ -30,6 +42,6 @@ namespace osu.Game.Storyboards
|
|||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
=> $"{LoopStartTime} x{LoopCount}";
|
=> $"{LoopStartTime} x{TotalIterations}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using osuTK;
|
|
||||||
using osu.Framework.Graphics;
|
|
||||||
using osu.Game.Storyboards.Drawables;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Game.Storyboards.Drawables;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Storyboards
|
namespace osu.Game.Storyboards
|
||||||
{
|
{
|
||||||
@ -78,9 +78,9 @@ namespace osu.Game.Storyboards
|
|||||||
InitialPosition = initialPosition;
|
InitialPosition = initialPosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CommandLoop AddLoop(double startTime, int loopCount)
|
public CommandLoop AddLoop(double startTime, int repeatCount)
|
||||||
{
|
{
|
||||||
var loop = new CommandLoop(startTime, loopCount);
|
var loop = new CommandLoop(startTime, repeatCount);
|
||||||
loops.Add(loop);
|
loops.Add(loop);
|
||||||
return loop;
|
return loop;
|
||||||
}
|
}
|
||||||
|
@ -84,7 +84,7 @@ namespace osu.Game.Tests.Visual
|
|||||||
|
|
||||||
if (autoplayMod != null)
|
if (autoplayMod != null)
|
||||||
{
|
{
|
||||||
DrawableRuleset?.SetReplayScore(autoplayMod.CreateReplayScore(GameplayBeatmap.PlayableBeatmap, Mods.Value));
|
DrawableRuleset?.SetReplayScore(autoplayMod.CreateReplayScore(GameplayState.Beatmap, Mods.Value));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,12 +20,12 @@
|
|||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="AutoMapper" Version="10.1.1" />
|
<PackageReference Include="AutoMapper" Version="10.1.1" />
|
||||||
<PackageReference Include="DiffPlex" Version="1.7.0" />
|
<PackageReference Include="DiffPlex" Version="1.7.0" />
|
||||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.36" />
|
<PackageReference Include="HtmlAgilityPack" Version="1.11.37" />
|
||||||
<PackageReference Include="Humanizer" Version="2.11.10" />
|
<PackageReference Include="Humanizer" Version="2.11.10" />
|
||||||
<PackageReference Include="MessagePack" Version="2.3.75" />
|
<PackageReference Include="MessagePack" Version="2.3.85" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="5.0.9" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="5.0.10" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="5.0.9" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="5.0.10" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="5.0.9" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="5.0.10" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
|
||||||
@ -35,10 +35,10 @@
|
|||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Realm" Version="10.5.0" />
|
<PackageReference Include="Realm" Version="10.6.0" />
|
||||||
<PackageReference Include="ppy.osu.Framework" Version="2021.929.0" />
|
<PackageReference Include="ppy.osu.Framework" Version="2021.929.0" />
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.918.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1004.0" />
|
||||||
<PackageReference Include="Sentry" Version="3.9.0" />
|
<PackageReference Include="Sentry" Version="3.9.4" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.29.0" />
|
<PackageReference Include="SharpCompress" Version="0.29.0" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||||
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
||||||
|
@ -71,7 +71,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.929.0" />
|
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.929.0" />
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.918.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1004.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
|
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
@ -99,6 +99,6 @@
|
|||||||
<PackageReference Include="SharpRaven" Version="2.4.0" />
|
<PackageReference Include="SharpRaven" Version="2.4.0" />
|
||||||
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
||||||
<PackageReference Include="ppy.osu.Framework.NativeLibs" Version="2021.805.0" ExcludeAssets="all" />
|
<PackageReference Include="ppy.osu.Framework.NativeLibs" Version="2021.805.0" ExcludeAssets="all" />
|
||||||
<PackageReference Include="Realm" Version="10.5.0" />
|
<PackageReference Include="Realm" Version="10.6.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
Loading…
Reference in New Issue
Block a user