Merge branch 'master' into wiki-article

This commit is contained in:
Bartłomiej Dach
2021-06-09 16:24:45 +02:00
committed by GitHub
74 changed files with 861 additions and 421 deletions

View File

@ -52,6 +52,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.525.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.525.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.607.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2021.608.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -20,7 +20,8 @@ namespace osu.Android
[Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false, LaunchMode = LaunchMode.SingleInstance, Exported = true)] [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false, LaunchMode = LaunchMode.SingleInstance, Exported = true)]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")]
[IntentFilter(new[] { Intent.ActionSend, Intent.ActionSendMultiple }, Categories = new[] { Intent.CategoryDefault }, DataMimeTypes = new[] { "application/zip", "application/octet-stream", "application/download", "application/x-zip", "application/x-zip-compressed" })] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataMimeType = "application/x-osu-archive")]
[IntentFilter(new[] { Intent.ActionSend, Intent.ActionSendMultiple }, Categories = new[] { Intent.CategoryDefault }, DataMimeTypes = new[] { "application/zip", "application/octet-stream", "application/download", "application/x-zip", "application/x-zip-compressed", "application/x-osu-archive" })]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryBrowsable, Intent.CategoryDefault }, DataSchemes = new[] { "osu", "osump" })] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryBrowsable, Intent.CategoryDefault }, DataSchemes = new[] { "osu", "osump" })]
public class OsuGameActivity : AndroidGameActivity public class OsuGameActivity : AndroidGameActivity
{ {

View File

@ -4,10 +4,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets.Catch.Mods; using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.Objects.Drawables;
@ -21,12 +19,6 @@ namespace osu.Game.Rulesets.Catch.Tests
{ {
public class TestSceneCatchModHidden : ModTestScene public class TestSceneCatchModHidden : ModTestScene
{ {
[BackgroundDependencyLoader]
private void load()
{
LocalConfig.SetValue(OsuSetting.IncreaseFirstObjectVisibility, false);
}
[Test] [Test]
public void TestJuiceStream() public void TestJuiceStream()
{ {

View File

@ -29,8 +29,7 @@ namespace osu.Game.Rulesets.Catch.Mods
} }
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
{ => ApplyNormalVisibilityState(hitObject, state);
}
protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state) protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state)
{ {

View File

@ -1,22 +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 osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Catch.Scoring
{
public class CatchHitWindows : HitWindows
{
public override bool IsHitResultAllowed(HitResult result)
{
switch (result)
{
case HitResult.Great:
case HitResult.Miss:
return true;
}
return false;
}
}
}

View File

@ -25,9 +25,9 @@ namespace osu.Game.Rulesets.Catch.UI
{ {
} }
protected override void SkinChanged(ISkinSource skin, bool allowFallback) protected override void SkinChanged(ISkinSource skin)
{ {
base.SkinChanged(skin, allowFallback); base.SkinChanged(skin);
ComboCounter?.UpdateCombo(currentCombo); ComboCounter?.UpdateCombo(currentCombo);
} }

View File

@ -396,9 +396,9 @@ namespace osu.Game.Rulesets.Catch.UI
private void updateTrailVisibility() => trails.DisplayTrail = Dashing || HyperDashing; private void updateTrailVisibility() => trails.DisplayTrail = Dashing || HyperDashing;
protected override void SkinChanged(ISkinSource skin, bool allowFallback) protected override void SkinChanged(ISkinSource skin)
{ {
base.SkinChanged(skin, allowFallback); base.SkinChanged(skin);
hyperDashColour = hyperDashColour =
skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDash)?.Value ?? skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDash)?.Value ??

View File

@ -69,10 +69,10 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
private void sourceChanged() private void sourceChanged()
{ {
isLegacySkin = new Lazy<bool>(() => Source.GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version) != null); isLegacySkin = new Lazy<bool>(() => FindProvider(s => s.GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version) != null) != null);
hasKeyTexture = new Lazy<bool>(() => Source.GetAnimation( hasKeyTexture = new Lazy<bool>(() => FindProvider(s => s.GetAnimation(
this.GetManiaSkinConfig<string>(LegacyManiaSkinConfigurationLookups.KeyImage, 0)?.Value s.GetManiaSkinConfig<string>(LegacyManiaSkinConfigurationLookups.KeyImage, 0)?.Value
?? "mania-key1", true, true) != null); ?? "mania-key1", true, true) != null) != null);
} }
public override Drawable GetDrawableComponent(ISkinComponent component) public override Drawable GetDrawableComponent(ISkinComponent component)

View File

@ -33,9 +33,9 @@ namespace osu.Game.Rulesets.Mania.UI.Components
Direction.BindValueChanged(onDirectionChanged, true); Direction.BindValueChanged(onDirectionChanged, true);
} }
protected override void SkinChanged(ISkinSource skin, bool allowFallback) protected override void SkinChanged(ISkinSource skin)
{ {
base.SkinChanged(skin, allowFallback); base.SkinChanged(skin);
UpdateHitPosition(); UpdateHitPosition();
} }

View File

@ -0,0 +1,10 @@
osu file format v14
[General]
Mode: 0
[TimingPoints]
0,300,4,1,2,100,1,0
[HitObjects]
444,320,1000,5,0,0:0:0:0:

View File

@ -39,18 +39,28 @@ namespace osu.Game.Rulesets.Osu.Tests
[Test] [Test]
public void TestLegacySmoothCursorTrail() public void TestLegacySmoothCursorTrail()
{ {
createTest(() => new LegacySkinContainer(false) createTest(() =>
{ {
Child = new LegacyCursorTrail() var skinContainer = new LegacySkinContainer(false);
var legacyCursorTrail = new LegacyCursorTrail(skinContainer);
skinContainer.Child = legacyCursorTrail;
return skinContainer;
}); });
} }
[Test] [Test]
public void TestLegacyDisjointCursorTrail() public void TestLegacyDisjointCursorTrail()
{ {
createTest(() => new LegacySkinContainer(true) createTest(() =>
{ {
Child = new LegacyCursorTrail() var skinContainer = new LegacySkinContainer(true);
var legacyCursorTrail = new LegacyCursorTrail(skinContainer);
skinContainer.Child = legacyCursorTrail;
return skinContainer;
}); });
} }
@ -102,6 +112,8 @@ namespace osu.Game.Rulesets.Osu.Tests
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => null; public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => null;
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => null;
public event Action SourceChanged public event Action SourceChanged
{ {
add { } add { }

View File

@ -113,6 +113,7 @@ namespace osu.Game.Rulesets.Osu.Tests
public Drawable GetDrawableComponent(ISkinComponent component) => null; public Drawable GetDrawableComponent(ISkinComponent component) => null;
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => null; public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => null;
public ISample GetSample(ISampleInfo sampleInfo) => null; public ISample GetSample(ISampleInfo sampleInfo) => null;
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => null;
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
{ {

View File

@ -0,0 +1,49 @@
// 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.Reflection;
using NUnit.Framework;
using osu.Framework.IO.Stores;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Osu.Tests
{
public class TestSceneOsuHitObjectSamples : HitObjectSampleTest
{
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
protected override IResourceStore<byte[]> RulesetResources => new DllResourceStore(Assembly.GetAssembly(typeof(TestSceneOsuHitObjectSamples)));
[TestCase("normal-hitnormal")]
[TestCase("hitnormal")]
public void TestDefaultCustomSampleFromBeatmap(string expectedSample)
{
SetupSkins(expectedSample, expectedSample);
CreateTestWithBeatmap("osu-hitobject-beatmap-custom-sample-bank.osu");
AssertBeatmapLookup(expectedSample);
}
[TestCase("normal-hitnormal")]
[TestCase("hitnormal")]
public void TestDefaultCustomSampleFromUserSkinFallback(string expectedSample)
{
SetupSkins(string.Empty, expectedSample);
CreateTestWithBeatmap("osu-hitobject-beatmap-custom-sample-bank.osu");
AssertUserLookup(expectedSample);
}
[TestCase("normal-hitnormal2")]
public void TestUserSkinLookupIgnoresSampleBank(string unwantedSample)
{
SetupSkins(string.Empty, unwantedSample);
CreateTestWithBeatmap("osu-hitobject-beatmap-custom-sample-bank.osu");
AssertNoLookup(unwantedSample);
}
}
}

View File

@ -166,6 +166,7 @@ namespace osu.Game.Rulesets.Osu.Tests
public TValue GetValue<TConfiguration, TValue>(Func<TConfiguration, TValue> query) where TConfiguration : SkinConfiguration => default; public TValue GetValue<TConfiguration, TValue>(Func<TConfiguration, TValue> query) where TConfiguration : SkinConfiguration => default;
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => null; public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => null;
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => null;
public event Action SourceChanged; public event Action SourceChanged;

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Utils; using osu.Framework.Utils;
@ -30,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Mods
private static readonly float border_distance_x = OsuPlayfield.BASE_SIZE.X * playfield_edge_ratio; private static readonly float border_distance_x = OsuPlayfield.BASE_SIZE.X * playfield_edge_ratio;
private static readonly float border_distance_y = OsuPlayfield.BASE_SIZE.Y * playfield_edge_ratio; private static readonly float border_distance_y = OsuPlayfield.BASE_SIZE.Y * playfield_edge_ratio;
private static readonly Vector2 playfield_middle = Vector2.Divide(OsuPlayfield.BASE_SIZE, 2); private static readonly Vector2 playfield_middle = OsuPlayfield.BASE_SIZE / 2;
private static readonly float playfield_diagonal = OsuPlayfield.BASE_SIZE.LengthFast; private static readonly float playfield_diagonal = OsuPlayfield.BASE_SIZE.LengthFast;
@ -74,22 +75,8 @@ namespace osu.Game.Rulesets.Osu.Mods
// update end position as it may have changed as a result of the position update. // update end position as it may have changed as a result of the position update.
current.EndPositionRandomised = current.PositionRandomised; current.EndPositionRandomised = current.PositionRandomised;
switch (hitObject) if (hitObject is Slider slider)
{ moveSliderIntoPlayfield(slider, current);
case Slider slider:
// Shift nested objects the same distance as the slider got shifted in the randomisation process
// so that moveSliderIntoPlayfield() can determine their relative distances to slider.Position and thus minMargin
shiftNestedObjects(slider, Vector2.Subtract(slider.Position, current.PositionOriginal));
var oldPos = new Vector2(slider.Position.X, slider.Position.Y);
moveSliderIntoPlayfield(slider, current);
// Shift them again to move them to their final position after the slider got moved into the playfield
shiftNestedObjects(slider, Vector2.Subtract(slider.Position, oldPos));
break;
}
previous = current; previous = current;
} }
@ -131,7 +118,7 @@ namespace osu.Game.Rulesets.Osu.Mods
current.AngleRad = (float)Math.Atan2(posRelativeToPrev.Y, posRelativeToPrev.X); current.AngleRad = (float)Math.Atan2(posRelativeToPrev.Y, posRelativeToPrev.X);
var position = Vector2.Add(previous.EndPositionRandomised, posRelativeToPrev); var position = previous.EndPositionRandomised + posRelativeToPrev;
// Move hit objects back into the playfield if they are outside of it, // Move hit objects back into the playfield if they are outside of it,
// which would sometimes happen during big jumps otherwise. // which would sometimes happen during big jumps otherwise.
@ -146,34 +133,41 @@ namespace osu.Game.Rulesets.Osu.Mods
/// </summary> /// </summary>
private void moveSliderIntoPlayfield(Slider slider, RandomObjectInfo currentObjectInfo) private void moveSliderIntoPlayfield(Slider slider, RandomObjectInfo currentObjectInfo)
{ {
// Min. distances from the slider's position to the playfield border var minMargin = getMinSliderMargin(slider);
var minMargin = new MarginPadding();
foreach (var hitObject in slider.NestedHitObjects.Where(o => o is SliderTick || o is SliderEndCircle)) slider.Position = new Vector2(
{ Math.Clamp(slider.Position.X, minMargin.Left, OsuPlayfield.BASE_SIZE.X - minMargin.Right),
if (!(hitObject is OsuHitObject osuHitObject)) Math.Clamp(slider.Position.Y, minMargin.Top, OsuPlayfield.BASE_SIZE.Y - minMargin.Bottom)
continue; );
var relativePos = Vector2.Subtract(osuHitObject.Position, slider.Position);
minMargin.Left = Math.Max(minMargin.Left, -relativePos.X);
minMargin.Right = Math.Max(minMargin.Right, relativePos.X);
minMargin.Top = Math.Max(minMargin.Top, -relativePos.Y);
minMargin.Bottom = Math.Max(minMargin.Bottom, relativePos.Y);
}
if (slider.Position.X < minMargin.Left)
slider.Position = new Vector2(minMargin.Left, slider.Position.Y);
else if (slider.Position.X + minMargin.Right > OsuPlayfield.BASE_SIZE.X)
slider.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - minMargin.Right, slider.Position.Y);
if (slider.Position.Y < minMargin.Top)
slider.Position = new Vector2(slider.Position.X, minMargin.Top);
else if (slider.Position.Y + minMargin.Bottom > OsuPlayfield.BASE_SIZE.Y)
slider.Position = new Vector2(slider.Position.X, OsuPlayfield.BASE_SIZE.Y - minMargin.Bottom);
currentObjectInfo.PositionRandomised = slider.Position; currentObjectInfo.PositionRandomised = slider.Position;
currentObjectInfo.EndPositionRandomised = slider.EndPosition; currentObjectInfo.EndPositionRandomised = slider.EndPosition;
shiftNestedObjects(slider, currentObjectInfo.PositionRandomised - currentObjectInfo.PositionOriginal);
}
/// <summary>
/// Calculates the min. distances from the <see cref="Slider"/>'s position to the playfield border for the slider to be fully inside of the playfield.
/// </summary>
private MarginPadding getMinSliderMargin(Slider slider)
{
var pathPositions = new List<Vector2>();
slider.Path.GetPathToProgress(pathPositions, 0, 1);
var minMargin = new MarginPadding();
foreach (var pos in pathPositions)
{
minMargin.Left = Math.Max(minMargin.Left, -pos.X);
minMargin.Right = Math.Max(minMargin.Right, pos.X);
minMargin.Top = Math.Max(minMargin.Top, -pos.Y);
minMargin.Bottom = Math.Max(minMargin.Bottom, pos.Y);
}
minMargin.Left = Math.Min(minMargin.Left, OsuPlayfield.BASE_SIZE.X - minMargin.Right);
minMargin.Top = Math.Min(minMargin.Top, OsuPlayfield.BASE_SIZE.Y - minMargin.Bottom);
return minMargin;
} }
/// <summary> /// <summary>
@ -188,7 +182,7 @@ namespace osu.Game.Rulesets.Osu.Mods
if (!(hitObject is OsuHitObject osuHitObject)) if (!(hitObject is OsuHitObject osuHitObject))
continue; continue;
osuHitObject.Position = Vector2.Add(osuHitObject.Position, shift); osuHitObject.Position += shift;
} }
} }

View File

@ -152,7 +152,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
while (Math.Abs(aimRotation - Arrow.Rotation) > 180) while (Math.Abs(aimRotation - Arrow.Rotation) > 180)
aimRotation += aimRotation < Arrow.Rotation ? 360 : -360; aimRotation += aimRotation < Arrow.Rotation ? 360 : -360;
if (!hasRotation) // The clock may be paused in a scenario like the editor.
if (!hasRotation || !Clock.IsRunning)
{ {
Arrow.Rotation = aimRotation; Arrow.Rotation = aimRotation;
hasRotation = true; hasRotation = true;

View File

@ -18,9 +18,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
} }
protected override void SkinChanged(ISkinSource skin, bool allowFallback) protected override void SkinChanged(ISkinSource skin)
{ {
base.SkinChanged(skin, allowFallback); base.SkinChanged(skin);
updateColour(); updateColour();
} }

View File

@ -128,5 +128,13 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
spmContainer.FadeIn(drawableSpinner.HitObject.TimeFadeIn); spmContainer.FadeIn(drawableSpinner.HitObject.TimeFadeIn);
} }
} }
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (drawableSpinner != null)
drawableSpinner.ApplyCustomUpdateState -= updateStateTransforms;
}
} }
} }

View File

@ -42,7 +42,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
private readonly IBindable<Color4> accentColour = new Bindable<Color4>(); private readonly IBindable<Color4> accentColour = new Bindable<Color4>();
private readonly IBindable<int> indexInCurrentCombo = new Bindable<int>(); private readonly IBindable<int> indexInCurrentCombo = new Bindable<int>();
private readonly IBindable<ArmedState> armedState = new Bindable<ArmedState>();
[Resolved] [Resolved]
private DrawableHitObject drawableObject { get; set; } private DrawableHitObject drawableObject { get; set; }
@ -54,7 +53,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
accentColour.BindTo(drawableObject.AccentColour); accentColour.BindTo(drawableObject.AccentColour);
indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable); indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable);
armedState.BindTo(drawableObject.State);
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -70,19 +68,18 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
indexInCurrentCombo.BindValueChanged(index => number.Text = (index.NewValue + 1).ToString(), true); indexInCurrentCombo.BindValueChanged(index => number.Text = (index.NewValue + 1).ToString(), true);
armedState.BindValueChanged(animate, true); drawableObject.ApplyCustomUpdateState += updateStateTransforms;
updateStateTransforms(drawableObject, drawableObject.State.Value);
} }
private void animate(ValueChangedEvent<ArmedState> state) private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
{ {
ClearTransforms(true);
using (BeginAbsoluteSequence(drawableObject.StateUpdateTime)) using (BeginAbsoluteSequence(drawableObject.StateUpdateTime))
glow.FadeOut(400); glow.FadeOut(400);
using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime)) using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
{ {
switch (state.NewValue) switch (state)
{ {
case ArmedState.Hit: case ArmedState.Hit:
const double flash_in = 40; const double flash_in = 40;
@ -109,5 +106,13 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
} }
} }
} }
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (drawableObject != null)
drawableObject.ApplyCustomUpdateState -= updateStateTransforms;
}
} }
} }

View File

@ -11,10 +11,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{ {
public class LegacyCursor : OsuCursorSprite public class LegacyCursor : OsuCursorSprite
{ {
private readonly ISkin skin;
private bool spin; private bool spin;
public LegacyCursor() public LegacyCursor(ISkin skin)
{ {
this.skin = skin;
Size = new Vector2(50); Size = new Vector2(50);
Anchor = Anchor.Centre; Anchor = Anchor.Centre;
@ -22,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(ISkinSource skin) private void load()
{ {
bool centre = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.CursorCentre)?.Value ?? true; bool centre = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.CursorCentre)?.Value ?? true;
spin = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.CursorRotate)?.Value ?? true; spin = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.CursorRotate)?.Value ?? true;

View File

@ -14,14 +14,20 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{ {
public class LegacyCursorTrail : CursorTrail public class LegacyCursorTrail : CursorTrail
{ {
private readonly ISkin skin;
private const double disjoint_trail_time_separation = 1000 / 60.0; private const double disjoint_trail_time_separation = 1000 / 60.0;
private bool disjointTrail; private bool disjointTrail;
private double lastTrailTime; private double lastTrailTime;
private IBindable<float> cursorSize; private IBindable<float> cursorSize;
public LegacyCursorTrail(ISkin skin)
{
this.skin = skin;
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(ISkinSource skin, OsuConfigManager config) private void load(OsuConfigManager config)
{ {
Texture = skin.GetTexture("cursortrail"); Texture = skin.GetTexture("cursortrail");
disjointTrail = skin.GetTexture("cursormiddle") == null; disjointTrail = skin.GetTexture("cursormiddle") == null;

View File

@ -20,6 +20,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{ {
public class LegacyMainCirclePiece : CompositeDrawable public class LegacyMainCirclePiece : CompositeDrawable
{ {
public override bool RemoveCompletedTransforms => false;
private readonly string priorityLookup; private readonly string priorityLookup;
private readonly bool hasNumber; private readonly bool hasNumber;
@ -39,7 +41,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
private readonly Bindable<Color4> accentColour = new Bindable<Color4>(); private readonly Bindable<Color4> accentColour = new Bindable<Color4>();
private readonly IBindable<int> indexInCurrentCombo = new Bindable<int>(); private readonly IBindable<int> indexInCurrentCombo = new Bindable<int>();
private readonly IBindable<ArmedState> armedState = new Bindable<ArmedState>();
[Resolved] [Resolved]
private DrawableHitObject drawableObject { get; set; } private DrawableHitObject drawableObject { get; set; }
@ -114,7 +115,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
accentColour.BindTo(drawableObject.AccentColour); accentColour.BindTo(drawableObject.AccentColour);
indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable); indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable);
armedState.BindTo(drawableObject.State);
Texture getTextureWithFallback(string name) Texture getTextureWithFallback(string name)
{ {
@ -140,18 +140,17 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
if (hasNumber) if (hasNumber)
indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true); indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true);
armedState.BindValueChanged(animate, true); drawableObject.ApplyCustomUpdateState += updateStateTransforms;
updateStateTransforms(drawableObject, drawableObject.State.Value);
} }
private void animate(ValueChangedEvent<ArmedState> state) private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
{ {
const double legacy_fade_duration = 240; const double legacy_fade_duration = 240;
ClearTransforms(true);
using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime)) using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
{ {
switch (state.NewValue) switch (state)
{ {
case ArmedState.Hit: case ArmedState.Hit:
circleSprites.FadeOut(legacy_fade_duration, Easing.Out); circleSprites.FadeOut(legacy_fade_duration, Easing.Out);
@ -176,5 +175,13 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
} }
} }
} }
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (drawableObject != null)
drawableObject.ApplyCustomUpdateState -= updateStateTransforms;
}
} }
} }

View File

@ -14,18 +14,21 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{ {
private readonly Drawable animationContent; private readonly Drawable animationContent;
private readonly ISkin skin;
private Sprite layerNd; private Sprite layerNd;
private Sprite layerSpec; private Sprite layerSpec;
public LegacySliderBall(Drawable animationContent) public LegacySliderBall(Drawable animationContent, ISkin skin)
{ {
this.animationContent = animationContent; this.animationContent = animationContent;
this.skin = skin;
AutoSizeAxes = Axes.Both; AutoSizeAxes = Axes.Both;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(ISkinSource skin) private void load()
{ {
var ballColour = skin.GetConfig<OsuSkinColour, Color4>(OsuSkinColour.SliderBall)?.Value ?? Color4.White; var ballColour = skin.GetConfig<OsuSkinColour, Color4>(OsuSkinColour.SliderBall)?.Value ?? Color4.White;

View File

@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
private void sourceChanged() private void sourceChanged()
{ {
hasHitCircle = new Lazy<bool>(() => Source.GetTexture("hitcircle") != null); hasHitCircle = new Lazy<bool>(() => FindProvider(s => s.GetTexture("hitcircle") != null) != null);
} }
public override Drawable GetDrawableComponent(ISkinComponent component) public override Drawable GetDrawableComponent(ISkinComponent component)
@ -49,13 +49,16 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
return followCircle; return followCircle;
case OsuSkinComponents.SliderBall: case OsuSkinComponents.SliderBall:
var sliderBallContent = this.GetAnimation("sliderb", true, true, animationSeparator: ""); // specular and nd layers must come from the same source as the ball texure.
var ballProvider = Source.FindProvider(s => s.GetTexture("sliderb") != null || s.GetTexture("sliderb0") != null);
var sliderBallContent = ballProvider.GetAnimation("sliderb", true, true, animationSeparator: "");
// todo: slider ball has a custom frame delay based on velocity // todo: slider ball has a custom frame delay based on velocity
// Math.Max((150 / Velocity) * GameBase.SIXTY_FRAME_TIME, GameBase.SIXTY_FRAME_TIME); // Math.Max((150 / Velocity) * GameBase.SIXTY_FRAME_TIME, GameBase.SIXTY_FRAME_TIME);
if (sliderBallContent != null) if (sliderBallContent != null)
return new LegacySliderBall(sliderBallContent); return new LegacySliderBall(sliderBallContent, ballProvider);
return null; return null;
@ -84,14 +87,18 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
return null; return null;
case OsuSkinComponents.Cursor: case OsuSkinComponents.Cursor:
if (Source.GetTexture("cursor") != null) var cursorProvider = Source.FindProvider(s => s.GetTexture("cursor") != null);
return new LegacyCursor();
if (cursorProvider != null)
return new LegacyCursor(cursorProvider);
return null; return null;
case OsuSkinComponents.CursorTrail: case OsuSkinComponents.CursorTrail:
if (Source.GetTexture("cursortrail") != null) var trailProvider = Source.FindProvider(s => s.GetTexture("cursortrail") != null);
return new LegacyCursorTrail();
if (trailProvider != null)
return new LegacyCursorTrail(trailProvider);
return null; return null;

View File

@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
Size = new Vector2(size); Size = new Vector2(size);
} }
protected override void SkinChanged(ISkinSource skin, bool allowFallback) protected override void SkinChanged(ISkinSource skin)
{ {
cursorExpand = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.CursorExpand)?.Value ?? true; cursorExpand = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.CursorExpand)?.Value ?? true;
} }

View File

@ -152,32 +152,35 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
throw new ArgumentOutOfRangeException(nameof(component), $"Invalid component type: {component}"); throw new ArgumentOutOfRangeException(nameof(component), $"Invalid component type: {component}");
} }
public override ISample GetSample(ISampleInfo sampleInfo) => Source.GetSample(new LegacyTaikoSampleInfo(sampleInfo)); public override ISample GetSample(ISampleInfo sampleInfo)
{
if (sampleInfo is HitSampleInfo hitSampleInfo)
return Source.GetSample(new LegacyTaikoSampleInfo(hitSampleInfo));
return base.GetSample(sampleInfo);
}
public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => Source.GetConfig<TLookup, TValue>(lookup); public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => Source.GetConfig<TLookup, TValue>(lookup);
private class LegacyTaikoSampleInfo : ISampleInfo private class LegacyTaikoSampleInfo : HitSampleInfo
{ {
private readonly ISampleInfo source; public LegacyTaikoSampleInfo(HitSampleInfo sampleInfo)
: base(sampleInfo.Name, sampleInfo.Bank, sampleInfo.Suffix, sampleInfo.Volume)
public LegacyTaikoSampleInfo(ISampleInfo source)
{ {
this.source = source;
} }
public IEnumerable<string> LookupNames public override IEnumerable<string> LookupNames
{ {
get get
{ {
foreach (var name in source.LookupNames) foreach (var name in base.LookupNames)
yield return name.Insert(name.LastIndexOf('/') + 1, "taiko-"); yield return name.Insert(name.LastIndexOf('/') + 1, "taiko-");
foreach (var name in source.LookupNames) foreach (var name in base.LookupNames)
yield return name; yield return name;
} }
} }
public int Volume => source.Volume;
} }
} }
} }

View File

@ -85,8 +85,12 @@ namespace osu.Game.Rulesets.Taiko.UI
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(ISkinSource skin) private void load(ISkinSource source)
{ {
ISkin skin = source.FindProvider(s => getAnimationFrame(s, state, 0) != null);
if (skin == null) return;
for (int frameIndex = 0; true; frameIndex++) for (int frameIndex = 0; true; frameIndex++)
{ {
var texture = getAnimationFrame(skin, state, frameIndex); var texture = getAnimationFrame(skin, state, frameIndex);
@ -112,8 +116,12 @@ namespace osu.Game.Rulesets.Taiko.UI
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(ISkinSource skin) private void load(ISkinSource source)
{ {
ISkin skin = source.FindProvider(s => getAnimationFrame(s, TaikoMascotAnimationState.Clear, 0) != null);
if (skin == null) return;
foreach (var frameIndex in clear_animation_sequence) foreach (var frameIndex in clear_animation_sequence)
{ {
var texture = getAnimationFrame(skin, TaikoMascotAnimationState.Clear, frameIndex); var texture = getAnimationFrame(skin, TaikoMascotAnimationState.Clear, frameIndex);

View File

@ -123,6 +123,8 @@ namespace osu.Game.Tests.Gameplay
public ISample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException(); public ISample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => null;
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
{ {
switch (lookup) switch (lookup)

View File

@ -61,6 +61,7 @@ namespace osu.Game.Tests.NonVisual.Skinning
public Drawable GetDrawableComponent(ISkinComponent component) => throw new NotSupportedException(); public Drawable GetDrawableComponent(ISkinComponent component) => throw new NotSupportedException();
public ISample GetSample(ISampleInfo sampleInfo) => throw new NotSupportedException(); public ISample GetSample(ISampleInfo sampleInfo) => throw new NotSupportedException();
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => throw new NotSupportedException(); public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => throw new NotSupportedException();
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => null;
} }
private class TestAnimationTimeReference : IAnimationTimeReference private class TestAnimationTimeReference : IAnimationTimeReference

View File

@ -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.IO; using System.IO;
using System.Linq; using System.Linq;
@ -222,6 +223,8 @@ namespace osu.Game.Tests.Skins
public ISample GetSample(ISampleInfo sampleInfo) => skin.GetSample(sampleInfo); public ISample GetSample(ISampleInfo sampleInfo) => skin.GetSample(sampleInfo);
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => skin.GetConfig<TLookup, TValue>(lookup); public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => skin.GetConfig<TLookup, TValue>(lookup);
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => skin.FindProvider(lookupFunction);
} }
} }
} }

View File

@ -0,0 +1,133 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Online.API;
using osu.Game.Screens;
using osu.Game.Screens.Backgrounds;
using osu.Game.Skinning;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Background
{
[TestFixture]
public class TestSceneBackgroundScreenDefault : OsuTestScene
{
private BackgroundScreenStack stack;
private BackgroundScreenDefault screen;
private Graphics.Backgrounds.Background getCurrentBackground() => screen.ChildrenOfType<Graphics.Backgrounds.Background>().FirstOrDefault();
[Resolved]
private SkinManager skins { get; set; }
[Resolved]
private OsuConfigManager config { get; set; }
[SetUpSteps]
public void SetUpSteps()
{
AddStep("create background stack", () => Child = stack = new BackgroundScreenStack());
AddStep("push default screen", () => stack.Push(screen = new BackgroundScreenDefault(false)));
AddUntilStep("wait for screen to load", () => screen.IsCurrentScreen());
}
[Test]
public void TestBackgroundTypeSwitch()
{
setSupporter(true);
setSourceMode(BackgroundSource.Beatmap);
AddUntilStep("is beatmap background", () => getCurrentBackground() is BeatmapBackground);
setSourceMode(BackgroundSource.BeatmapWithStoryboard);
AddUntilStep("is storyboard background", () => getCurrentBackground() is BeatmapBackgroundWithStoryboard);
setSourceMode(BackgroundSource.Skin);
AddUntilStep("is default background", () => getCurrentBackground().GetType() == typeof(Graphics.Backgrounds.Background));
setCustomSkin();
AddUntilStep("is skin background", () => getCurrentBackground() is SkinBackground);
}
[Test]
public void TestTogglingSupporterTogglesBeatmapBackground()
{
setSourceMode(BackgroundSource.Beatmap);
setSupporter(true);
AddUntilStep("is beatmap background", () => getCurrentBackground() is BeatmapBackground);
setSupporter(false);
AddUntilStep("is default background", () => !(getCurrentBackground() is BeatmapBackground));
setSupporter(true);
AddUntilStep("is beatmap background", () => getCurrentBackground() is BeatmapBackground);
}
[TestCase(BackgroundSource.Beatmap, typeof(BeatmapBackground))]
[TestCase(BackgroundSource.BeatmapWithStoryboard, typeof(BeatmapBackgroundWithStoryboard))]
[TestCase(BackgroundSource.Skin, typeof(SkinBackground))]
public void TestBackgroundDoesntReloadOnNoChange(BackgroundSource source, Type backgroundType)
{
Graphics.Backgrounds.Background last = null;
setSourceMode(source);
setSupporter(true);
if (source == BackgroundSource.Skin)
setCustomSkin();
AddUntilStep("wait for beatmap background to be loaded", () => (last = getCurrentBackground())?.GetType() == backgroundType);
AddAssert("next doesn't load new background", () => screen.Next() == false);
// doesn't really need to be checked but might as well.
AddWaitStep("wait a bit", 5);
AddUntilStep("ensure same background instance", () => last == getCurrentBackground());
}
[Test]
public void TestBackgroundCyclingOnDefaultSkin([Values] bool supporter)
{
Graphics.Backgrounds.Background last = null;
setSourceMode(BackgroundSource.Skin);
setSupporter(supporter);
setDefaultSkin();
AddUntilStep("wait for beatmap background to be loaded", () => (last = getCurrentBackground())?.GetType() == typeof(Graphics.Backgrounds.Background));
AddAssert("next cycles background", () => screen.Next());
// doesn't really need to be checked but might as well.
AddWaitStep("wait a bit", 5);
AddUntilStep("ensure different background instance", () => last != getCurrentBackground());
}
private void setSourceMode(BackgroundSource source) =>
AddStep($"set background mode to {source}", () => config.SetValue(OsuSetting.MenuBackgroundSource, source));
private void setSupporter(bool isSupporter) =>
AddStep($"set supporter {isSupporter}", () => ((DummyAPIAccess)API).LocalUser.Value = new User
{
IsSupporter = isSupporter,
Id = API.LocalUser.Value.Id + 1,
});
private void setCustomSkin()
{
// feign a skin switch. this doesn't do anything except force CurrentSkin to become a LegacySkin.
AddStep("set custom skin", () => skins.CurrentSkinInfo.Value = new SkinInfo { ID = 5 });
}
private void setDefaultSkin() => AddStep("set default skin", () => skins.CurrentSkinInfo.SetDefault());
[TearDownSteps]
public void TearDown() => setDefaultSkin();
}
}

View File

@ -4,14 +4,15 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Catch.Scoring;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Scoring; using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@ -29,15 +30,22 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
public class TestSceneHitErrorMeter : OsuTestScene public class TestSceneHitErrorMeter : OsuTestScene
{ {
[Cached] [Cached(typeof(ScoreProcessor))]
private ScoreProcessor scoreProcessor = new ScoreProcessor(); private TestScoreProcessor scoreProcessor = new TestScoreProcessor();
[Cached(typeof(DrawableRuleset))] [Cached(typeof(DrawableRuleset))]
private TestDrawableRuleset drawableRuleset = new TestDrawableRuleset(); private TestDrawableRuleset drawableRuleset = new TestDrawableRuleset();
public TestSceneHitErrorMeter() [SetUpSteps]
public void SetUp()
{ {
recreateDisplay(new OsuHitWindows(), 5); AddStep("reset score processor", () => scoreProcessor.Reset());
}
[Test]
public void TestBasic()
{
AddStep("create display", () => recreateDisplay(new OsuHitWindows(), 5));
AddRepeatStep("New random judgement", () => newJudgement(), 40); AddRepeatStep("New random judgement", () => newJudgement(), 40);
@ -45,12 +53,11 @@ namespace osu.Game.Tests.Visual.Gameplay
AddRepeatStep("New max positive", () => newJudgement(drawableRuleset.HitWindows.WindowFor(HitResult.Meh)), 20); AddRepeatStep("New max positive", () => newJudgement(drawableRuleset.HitWindows.WindowFor(HitResult.Meh)), 20);
AddStep("New fixed judgement (50ms)", () => newJudgement(50)); AddStep("New fixed judgement (50ms)", () => newJudgement(50));
ScheduledDelegate del = null;
AddStep("Judgement barrage", () => AddStep("Judgement barrage", () =>
{ {
int runCount = 0; int runCount = 0;
ScheduledDelegate del = null;
del = Scheduler.AddDelayed(() => del = Scheduler.AddDelayed(() =>
{ {
newJudgement(runCount++ / 10f); newJudgement(runCount++ / 10f);
@ -60,6 +67,7 @@ namespace osu.Game.Tests.Visual.Gameplay
del?.Cancel(); del?.Cancel();
}, 10, true); }, 10, true);
}); });
AddUntilStep("wait for barrage", () => del.Cancelled);
} }
[Test] [Test]
@ -84,10 +92,49 @@ namespace osu.Game.Tests.Visual.Gameplay
} }
[Test] [Test]
public void TestCatch() public void TestEmpty()
{ {
AddStep("OD 1", () => recreateDisplay(new CatchHitWindows(), 1)); AddStep("empty windows", () => recreateDisplay(HitWindows.Empty, 5));
AddStep("OD 10", () => recreateDisplay(new CatchHitWindows(), 10));
AddStep("hit", () => newJudgement());
AddAssert("no bars added", () => !this.ChildrenOfType<BarHitErrorMeter.JudgementLine>().Any());
AddAssert("circle added", () =>
this.ChildrenOfType<ColourHitErrorMeter>().All(
meter => meter.ChildrenOfType<ColourHitErrorMeter.HitErrorCircle>().Count() == 1));
AddStep("miss", () => newJudgement(50, HitResult.Miss));
AddAssert("no bars added", () => !this.ChildrenOfType<BarHitErrorMeter.JudgementLine>().Any());
AddAssert("circle added", () =>
this.ChildrenOfType<ColourHitErrorMeter>().All(
meter => meter.ChildrenOfType<ColourHitErrorMeter.HitErrorCircle>().Count() == 2));
}
[Test]
public void TestBonus()
{
AddStep("OD 1", () => recreateDisplay(new OsuHitWindows(), 1));
AddStep("small bonus", () => newJudgement(result: HitResult.SmallBonus));
AddAssert("no bars added", () => !this.ChildrenOfType<BarHitErrorMeter.JudgementLine>().Any());
AddAssert("no circle added", () => !this.ChildrenOfType<ColourHitErrorMeter.HitErrorCircle>().Any());
AddStep("large bonus", () => newJudgement(result: HitResult.LargeBonus));
AddAssert("no bars added", () => !this.ChildrenOfType<BarHitErrorMeter.JudgementLine>().Any());
AddAssert("no circle added", () => !this.ChildrenOfType<ColourHitErrorMeter.HitErrorCircle>().Any());
}
[Test]
public void TestIgnore()
{
AddStep("OD 1", () => recreateDisplay(new OsuHitWindows(), 1));
AddStep("ignore hit", () => newJudgement(result: HitResult.IgnoreHit));
AddAssert("no bars added", () => !this.ChildrenOfType<BarHitErrorMeter.JudgementLine>().Any());
AddAssert("no circle added", () => !this.ChildrenOfType<ColourHitErrorMeter.HitErrorCircle>().Any());
AddStep("ignore miss", () => newJudgement(result: HitResult.IgnoreMiss));
AddAssert("no bars added", () => !this.ChildrenOfType<BarHitErrorMeter.JudgementLine>().Any());
AddAssert("no circle added", () => !this.ChildrenOfType<ColourHitErrorMeter.HitErrorCircle>().Any());
} }
private void recreateDisplay(HitWindows hitWindows, float overallDifficulty) private void recreateDisplay(HitWindows hitWindows, float overallDifficulty)
@ -154,12 +201,12 @@ namespace osu.Game.Tests.Visual.Gameplay
}); });
} }
private void newJudgement(double offset = 0) private void newJudgement(double offset = 0, HitResult result = HitResult.Perfect)
{ {
scoreProcessor.ApplyResult(new JudgementResult(new HitCircle { HitWindows = drawableRuleset.HitWindows }, new Judgement()) scoreProcessor.ApplyResult(new JudgementResult(new HitCircle { HitWindows = drawableRuleset.HitWindows }, new Judgement())
{ {
TimeOffset = offset == 0 ? RNG.Next(-150, 150) : offset, TimeOffset = offset == 0 ? RNG.Next(-150, 150) : offset,
Type = HitResult.Perfect, Type = result,
}); });
} }
@ -199,5 +246,10 @@ namespace osu.Game.Tests.Visual.Gameplay
public override void CancelResume() => throw new NotImplementedException(); public override void CancelResume() => throw new NotImplementedException();
} }
private class TestScoreProcessor : ScoreProcessor
{
public void Reset() => base.Reset(false);
}
} }
} }

View File

@ -42,9 +42,9 @@ namespace osu.Game.Tests.Visual.Gameplay
Spacing = new Vector2(10), Spacing = new Vector2(10),
Children = new[] Children = new[]
{ {
new ExposedSkinnableDrawable("default", _ => new DefaultBox(), _ => true), new ExposedSkinnableDrawable("default", _ => new DefaultBox()),
new ExposedSkinnableDrawable("available", _ => new DefaultBox(), _ => true), new ExposedSkinnableDrawable("available", _ => new DefaultBox()),
new ExposedSkinnableDrawable("available", _ => new DefaultBox(), _ => true, ConfineMode.NoScaling) new ExposedSkinnableDrawable("available", _ => new DefaultBox(), ConfineMode.NoScaling)
} }
}, },
}; };
@ -73,9 +73,9 @@ namespace osu.Game.Tests.Visual.Gameplay
Spacing = new Vector2(10), Spacing = new Vector2(10),
Children = new[] Children = new[]
{ {
new ExposedSkinnableDrawable("default", _ => new DefaultBox(), _ => true), new ExposedSkinnableDrawable("default", _ => new DefaultBox()),
new ExposedSkinnableDrawable("available", _ => new DefaultBox(), _ => true, ConfineMode.ScaleToFit), new ExposedSkinnableDrawable("available", _ => new DefaultBox(), ConfineMode.ScaleToFit),
new ExposedSkinnableDrawable("available", _ => new DefaultBox(), _ => true, ConfineMode.NoScaling) new ExposedSkinnableDrawable("available", _ => new DefaultBox(), ConfineMode.NoScaling)
} }
}, },
}; };
@ -100,7 +100,7 @@ namespace osu.Game.Tests.Visual.Gameplay
Child = new SkinProvidingContainer(secondarySource) Child = new SkinProvidingContainer(secondarySource)
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Child = consumer = new SkinConsumer("test", name => new NamedBox("Default Implementation"), source => true) Child = consumer = new SkinConsumer("test", name => new NamedBox("Default Implementation"))
} }
}; };
}); });
@ -129,7 +129,7 @@ namespace osu.Game.Tests.Visual.Gameplay
}; };
}); });
AddStep("add permissive", () => target.Add(consumer = new SkinConsumer("test", name => new NamedBox("Default Implementation"), source => true))); AddStep("add permissive", () => target.Add(consumer = new SkinConsumer("test", name => new NamedBox("Default Implementation"))));
AddAssert("consumer using override source", () => consumer.Drawable is SecondarySourceBox); AddAssert("consumer using override source", () => consumer.Drawable is SecondarySourceBox);
AddAssert("skinchanged only called once", () => consumer.SkinChangedCount == 1); AddAssert("skinchanged only called once", () => consumer.SkinChangedCount == 1);
} }
@ -152,7 +152,7 @@ namespace osu.Game.Tests.Visual.Gameplay
}; };
}); });
AddStep("add permissive", () => target.Add(consumer = new SkinConsumer("test", name => new NamedBox("Default Implementation"), source => true))); AddStep("add permissive", () => target.Add(consumer = new SkinConsumer("test", name => new NamedBox("Default Implementation"))));
AddAssert("consumer using override source", () => consumer.Drawable is SecondarySourceBox); AddAssert("consumer using override source", () => consumer.Drawable is SecondarySourceBox);
AddStep("disable", () => target.Disable()); AddStep("disable", () => target.Disable());
AddAssert("consumer using base source", () => consumer.Drawable is BaseSourceBox); AddAssert("consumer using base source", () => consumer.Drawable is BaseSourceBox);
@ -180,9 +180,8 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
public new Drawable Drawable => base.Drawable; public new Drawable Drawable => base.Drawable;
public ExposedSkinnableDrawable(string name, Func<ISkinComponent, Drawable> defaultImplementation, Func<ISkinSource, bool> allowFallback = null, public ExposedSkinnableDrawable(string name, Func<ISkinComponent, Drawable> defaultImplementation, ConfineMode confineMode = ConfineMode.ScaleToFit)
ConfineMode confineMode = ConfineMode.ScaleToFit) : base(new TestSkinComponent(name), defaultImplementation, confineMode)
: base(new TestSkinComponent(name), defaultImplementation, allowFallback, confineMode)
{ {
} }
} }
@ -250,14 +249,14 @@ namespace osu.Game.Tests.Visual.Gameplay
public new Drawable Drawable => base.Drawable; public new Drawable Drawable => base.Drawable;
public int SkinChangedCount { get; private set; } public int SkinChangedCount { get; private set; }
public SkinConsumer(string name, Func<ISkinComponent, Drawable> defaultImplementation, Func<ISkinSource, bool> allowFallback = null) public SkinConsumer(string name, Func<ISkinComponent, Drawable> defaultImplementation)
: base(new TestSkinComponent(name), defaultImplementation, allowFallback) : base(new TestSkinComponent(name), defaultImplementation)
{ {
} }
protected override void SkinChanged(ISkinSource skin, bool allowFallback) protected override void SkinChanged(ISkinSource skin)
{ {
base.SkinChanged(skin, allowFallback); base.SkinChanged(skin);
SkinChangedCount++; SkinChangedCount++;
} }
} }
@ -301,6 +300,8 @@ namespace osu.Game.Tests.Visual.Gameplay
public ISample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException(); public ISample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => throw new NotImplementedException(); public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => throw new NotImplementedException();
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => throw new NotImplementedException();
} }
private class SecondarySource : ISkin private class SecondarySource : ISkin
@ -312,6 +313,8 @@ namespace osu.Game.Tests.Visual.Gameplay
public ISample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException(); public ISample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => throw new NotImplementedException(); public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => throw new NotImplementedException();
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => throw new NotImplementedException();
} }
[Cached(typeof(ISkinSource))] [Cached(typeof(ISkinSource))]
@ -325,6 +328,8 @@ namespace osu.Game.Tests.Visual.Gameplay
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => throw new NotImplementedException(); public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => throw new NotImplementedException();
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => throw new NotImplementedException();
public event Action SourceChanged public event Action SourceChanged
{ {
add { } add { }

View File

@ -29,14 +29,13 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
AddStep("setup hierarchy", () => AddStep("setup hierarchy", () =>
{ {
Children = new Drawable[] Child = skinSource = new TestSkinSourceContainer
{ {
skinSource = new TestSkinSourceContainer RelativeSizeAxes = Axes.Both,
{
RelativeSizeAxes = Axes.Both,
Child = skinnableSound = new PausableSkinnableSound(new SampleInfo("Gameplay/normal-sliderslide"))
},
}; };
// has to be added after the hierarchy above else the `ISkinSource` dependency won't be cached.
skinSource.Add(skinnableSound = new PausableSkinnableSound(new SampleInfo("Gameplay/normal-sliderslide")));
}); });
} }
@ -147,6 +146,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => source?.GetTexture(componentName, wrapModeS, wrapModeT); public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => source?.GetTexture(componentName, wrapModeS, wrapModeT);
public ISample GetSample(ISampleInfo sampleInfo) => source?.GetSample(sampleInfo); public ISample GetSample(ISampleInfo sampleInfo) => source?.GetSample(sampleInfo);
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => source?.GetConfig<TLookup, TValue>(lookup); public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => source?.GetConfig<TLookup, TValue>(lookup);
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => source?.FindProvider(lookupFunction);
public void TriggerSourceChanged() public void TriggerSourceChanged()
{ {

View File

@ -9,16 +9,19 @@ using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Mods; using osu.Game.Overlays.Mods;
using osu.Game.Overlays.Toolbar; using osu.Game.Overlays.Toolbar;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
using osu.Game.Screens.Select; using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Options; using osu.Game.Screens.Select.Options;
using osu.Game.Tests.Beatmaps.IO; using osu.Game.Tests.Beatmaps.IO;
using osu.Game.Tests.Visual.Multiplayer;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
@ -152,6 +155,14 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("wait for track", () => !Game.MusicController.CurrentTrack.IsDummyDevice && Game.MusicController.IsPlaying); AddUntilStep("wait for track", () => !Game.MusicController.CurrentTrack.IsDummyDevice && Game.MusicController.IsPlaying);
} }
[Test]
public void TestPushSongSelectAndPressBackButtonImmediately()
{
AddStep("push song select", () => Game.ScreenStack.Push(new TestPlaySongSelect()));
AddStep("press back button", () => Game.ChildrenOfType<BackButton>().First().Action());
AddWaitStep("wait two frames", 2);
}
[Test] [Test]
public void TestExitSongSelectWithClick() public void TestExitSongSelectWithClick()
{ {
@ -298,6 +309,18 @@ namespace osu.Game.Tests.Visual.Navigation
AddAssert("Toolbar is hidden", () => Game.Toolbar.State.Value == Visibility.Hidden); AddAssert("Toolbar is hidden", () => Game.Toolbar.State.Value == Visibility.Hidden);
} }
[Test]
public void TestPushMatchSubScreenAndPressBackButtonImmediately()
{
TestMultiplayer multiplayer = null;
PushAndConfirm(() => multiplayer = new TestMultiplayer());
AddStep("open room", () => multiplayer.OpenNewRoom());
AddStep("press back button", () => Game.ChildrenOfType<BackButton>().First().Action());
AddWaitStep("wait two frames", 2);
}
private void pushEscape() => private void pushEscape() =>
AddStep("Press escape", () => InputManager.Key(Key.Escape)); AddStep("Press escape", () => InputManager.Key(Key.Escape));
@ -322,5 +345,18 @@ namespace osu.Game.Tests.Visual.Navigation
protected override bool DisplayStableImportPrompt => false; protected override bool DisplayStableImportPrompt => false;
} }
private class TestMultiplayer : Screens.OnlinePlay.Multiplayer.Multiplayer
{
[Cached(typeof(MultiplayerClient))]
public readonly TestMultiplayerClient Client;
public TestMultiplayer()
{
Client = new TestMultiplayerClient((TestMultiplayerRoomManager)RoomManager);
}
protected override RoomManager CreateRoomManager() => new TestMultiplayerRoomManager();
}
} }
} }

View File

@ -325,6 +325,7 @@ namespace osu.Game.Beatmaps
public ISkin Skin => skin.Value; public ISkin Skin => skin.Value;
protected abstract ISkin GetSkin(); protected abstract ISkin GetSkin();
private readonly RecyclableLazy<ISkin> skin; private readonly RecyclableLazy<ISkin> skin;
public abstract Stream GetStream(string storagePath); public abstract Stream GetStream(string storagePath);

View File

@ -264,14 +264,18 @@ namespace osu.Game.Collections
using (var sw = new SerializationWriter(storage.GetStream(database_name, FileAccess.Write))) using (var sw = new SerializationWriter(storage.GetStream(database_name, FileAccess.Write)))
{ {
sw.Write(database_version); sw.Write(database_version);
sw.Write(Collections.Count);
foreach (var c in Collections) var collectionsCopy = Collections.ToArray();
sw.Write(collectionsCopy.Length);
foreach (var c in collectionsCopy)
{ {
sw.Write(c.Name.Value); sw.Write(c.Name.Value);
sw.Write(c.Beatmaps.Count);
foreach (var b in c.Beatmaps) var beatmapsCopy = c.Beatmaps.ToArray();
sw.Write(beatmapsCopy.Length);
foreach (var b in beatmapsCopy)
sw.Write(b.MD5Hash); sw.Write(b.MD5Hash);
} }
} }

View File

@ -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 osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -14,7 +15,7 @@ namespace osu.Game.Graphics.Backgrounds
/// <summary> /// <summary>
/// A background which offers blurring via a <see cref="BufferedContainer"/> on demand. /// A background which offers blurring via a <see cref="BufferedContainer"/> on demand.
/// </summary> /// </summary>
public class Background : CompositeDrawable public class Background : CompositeDrawable, IEquatable<Background>
{ {
private const float blur_scale = 0.5f; private const float blur_scale = 0.5f;
@ -71,5 +72,14 @@ namespace osu.Game.Graphics.Backgrounds
bufferedContainer?.BlurTo(newBlurSigma * blur_scale, duration, easing); bufferedContainer?.BlurTo(newBlurSigma * blur_scale, duration, easing);
} }
public virtual bool Equals(Background other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return other.GetType() == GetType()
&& other.textureName == textureName;
}
} }
} }

View File

@ -24,5 +24,14 @@ namespace osu.Game.Graphics.Backgrounds
{ {
Sprite.Texture = Beatmap?.Background ?? textures.Get(fallbackTextureName); Sprite.Texture = Beatmap?.Background ?? textures.Get(fallbackTextureName);
} }
public override bool Equals(Background other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return other.GetType() == GetType()
&& ((BeatmapBackground)other).Beatmap == Beatmap;
}
} }
} }

View File

@ -0,0 +1,34 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Game.Skinning;
namespace osu.Game.Graphics.Backgrounds
{
internal class SkinBackground : Background
{
private readonly Skin skin;
public SkinBackground(Skin skin, string fallbackTextureName)
: base(fallbackTextureName)
{
this.skin = skin;
}
[BackgroundDependencyLoader]
private void load()
{
Sprite.Texture = skin.GetTexture("menu-background") ?? Sprite.Texture;
}
public override bool Equals(Background other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return other.GetType() == GetType()
&& ((SkinBackground)other).skin.SkinInfo.Equals(skin.SkinInfo);
}
}
}

View File

@ -57,12 +57,6 @@ namespace osu.Game.Graphics.Backgrounds
} }
} }
/// <summary>
/// Whether we want to expire triangles as they exit our draw area completely.
/// </summary>
[Obsolete("Unused.")] // Can be removed 20210518
protected virtual bool ExpireOffScreenTriangles => true;
/// <summary> /// <summary>
/// Whether we should create new triangles as others expire. /// Whether we should create new triangles as others expire.
/// </summary> /// </summary>

View File

@ -21,7 +21,12 @@ namespace osu.Game.Online.API.Requests.Responses
{ {
var ruleset = rulesets.GetRuleset(OnlineRulesetID); var ruleset = rulesets.GetRuleset(OnlineRulesetID);
var mods = Mods != null ? ruleset.CreateInstance().GetAllMods().Where(mod => Mods.Contains(mod.Acronym)).ToArray() : Array.Empty<Mod>(); var rulesetInstance = ruleset.CreateInstance();
var mods = Mods != null ? rulesetInstance.GetAllMods().Where(mod => Mods.Contains(mod.Acronym)).ToArray() : Array.Empty<Mod>();
// all API scores provided by this class are considered to be legacy.
mods = mods.Append(rulesetInstance.GetAllMods().OfType<ModClassic>().Single()).ToArray();
var scoreInfo = new ScoreInfo var scoreInfo = new ScoreInfo
{ {
@ -38,7 +43,6 @@ namespace osu.Game.Online.API.Requests.Responses
Rank = Rank, Rank = Rank,
Ruleset = ruleset, Ruleset = ruleset,
Mods = mods, Mods = mods,
IsLegacyScore = true
}; };
if (Statistics != null) if (Statistics != null)

View File

@ -651,9 +651,10 @@ namespace osu.Game
Origin = Anchor.BottomLeft, Origin = Anchor.BottomLeft,
Action = () => Action = () =>
{ {
var currentScreen = ScreenStack.CurrentScreen as IOsuScreen; if (!(ScreenStack.CurrentScreen is IOsuScreen currentScreen))
return;
if (currentScreen?.AllowBackButton == true && !currentScreen.OnBackButton()) if (!((Drawable)currentScreen).IsLoaded || (currentScreen.AllowBackButton && !currentScreen.OnBackButton()))
ScreenStack.Exit(); ScreenStack.Exit();
} }
}, },

View File

@ -1,7 +1,6 @@
// 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.Diagnostics; using System.Diagnostics;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -32,18 +31,6 @@ namespace osu.Game.Rulesets.Judgements
private readonly Container aboveHitObjectsContent; private readonly Container aboveHitObjectsContent;
/// <summary>
/// Duration of initial fade in.
/// </summary>
[Obsolete("Apply any animations manually via ApplyHitAnimations / ApplyMissAnimations. Defaults were moved inside skinned components.")]
protected virtual double FadeInDuration => 100;
/// <summary>
/// Duration to wait until fade out begins. Defaults to <see cref="FadeInDuration"/>.
/// </summary>
[Obsolete("Apply any animations manually via ApplyHitAnimations / ApplyMissAnimations. Defaults were moved inside skinned components.")]
protected virtual double FadeOutDelay => FadeInDuration;
/// <summary> /// <summary>
/// Creates a drawable which visualises a <see cref="Judgements.Judgement"/>. /// Creates a drawable which visualises a <see cref="Judgements.Judgement"/>.
/// </summary> /// </summary>

View File

@ -1,7 +1,6 @@
// 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 osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
@ -69,14 +68,6 @@ namespace osu.Game.Rulesets.Judgements
/// </summary> /// </summary>
public double MaxHealthIncrease => HealthIncreaseFor(MaxResult); public double MaxHealthIncrease => HealthIncreaseFor(MaxResult);
/// <summary>
/// Retrieves the numeric score representation of a <see cref="HitResult"/>.
/// </summary>
/// <param name="result">The <see cref="HitResult"/> to find the numeric score representation for.</param>
/// <returns>The numeric score representation of <paramref name="result"/>.</returns>
[Obsolete("Has no effect. Use ToNumericResult(HitResult) (standardised across all rulesets).")] // Can be made non-virtual 20210328
protected virtual int NumericResultFor(HitResult result) => ToNumericResult(result);
/// <summary> /// <summary>
/// Retrieves the numeric score representation of a <see cref="JudgementResult"/>. /// Retrieves the numeric score representation of a <see cref="JudgementResult"/>.
/// </summary> /// </summary>

View File

@ -192,7 +192,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
/// <summary> /// <summary>
/// Applies a hit object to be represented by this <see cref="DrawableHitObject"/>. /// Applies a hit object to be represented by this <see cref="DrawableHitObject"/>.
/// </summary> /// </summary>
[Obsolete("Use either overload of Apply that takes a single argument of type HitObject or HitObjectLifetimeEntry")] [Obsolete("Use either overload of Apply that takes a single argument of type HitObject or HitObjectLifetimeEntry")] // Can be removed 20211021.
public void Apply([NotNull] HitObject hitObject, [CanBeNull] HitObjectLifetimeEntry lifetimeEntry) public void Apply([NotNull] HitObject hitObject, [CanBeNull] HitObjectLifetimeEntry lifetimeEntry)
{ {
if (lifetimeEntry != null) if (lifetimeEntry != null)
@ -409,11 +409,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
using (BeginAbsoluteSequence(StateUpdateTime, true)) using (BeginAbsoluteSequence(StateUpdateTime, true))
UpdateStartTimeStateTransforms(); UpdateStartTimeStateTransforms();
#pragma warning disable 618
using (BeginAbsoluteSequence(StateUpdateTime + (Result?.TimeOffset ?? 0), true))
UpdateStateTransforms(newState);
#pragma warning restore 618
using (BeginAbsoluteSequence(HitStateUpdateTime, true)) using (BeginAbsoluteSequence(HitStateUpdateTime, true))
UpdateHitStateTransforms(newState); UpdateHitStateTransforms(newState);
@ -447,7 +442,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
/// By default, this will fade in the object from zero with no duration. /// By default, this will fade in the object from zero with no duration.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This is called once before every <see cref="UpdateStateTransforms"/>. This is to ensure a good state in the case /// This is called once before every <see cref="UpdateHitStateTransforms"/>. This is to ensure a good state in the case
/// the <see cref="JudgementResult.TimeOffset"/> was negative and potentially altered the pre-hit transforms. /// the <see cref="JudgementResult.TimeOffset"/> was negative and potentially altered the pre-hit transforms.
/// </remarks> /// </remarks>
protected virtual void UpdateInitialTransforms() protected virtual void UpdateInitialTransforms()
@ -455,16 +450,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
this.FadeInFromZero(); this.FadeInFromZero();
} }
/// <summary>
/// Apply transforms based on the current <see cref="ArmedState"/>. Previous states are automatically cleared.
/// In the case of a non-idle <see cref="ArmedState"/>, and if <see cref="Drawable.LifetimeEnd"/> was not set during this call, <see cref="Drawable.Expire"/> will be invoked.
/// </summary>
/// <param name="state">The new armed state.</param>
[Obsolete("Use UpdateStartTimeStateTransforms and UpdateHitStateTransforms instead")] // Can be removed 20210504
protected virtual void UpdateStateTransforms(ArmedState state)
{
}
/// <summary> /// <summary>
/// Apply passive transforms at the <see cref="HitObject"/>'s StartTime. /// Apply passive transforms at the <see cref="HitObject"/>'s StartTime.
/// This is called each time <see cref="State"/> changes. /// This is called each time <see cref="State"/> changes.
@ -520,23 +505,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
AccentColour.Value = combo.GetComboColour(comboColours); AccentColour.Value = combo.GetComboColour(comboColours);
} }
/// <summary>
/// Called to retrieve the combo colour. Automatically assigned to <see cref="AccentColour"/>.
/// Defaults to using <see cref="IHasComboInformation.ComboIndex"/> to decide on a colour.
/// </summary>
/// <remarks>
/// This will only be called if the <see cref="HitObject"/> implements <see cref="IHasComboInformation"/>.
/// </remarks>
/// <param name="comboColours">A list of combo colours provided by the beatmap or skin. Can be null if not available.</param>
[Obsolete("Unused. Implement IHasComboInformation and IHasComboInformation.GetComboColour() on the HitObject model instead.")] // Can be removed 20210527
protected virtual Color4 GetComboColour(IReadOnlyList<Color4> comboColours)
{
if (!(HitObject is IHasComboInformation combo))
throw new InvalidOperationException($"{nameof(HitObject)} must implement {nameof(IHasComboInformation)}");
return comboColours?.Count > 0 ? comboColours[combo.ComboIndex % comboColours.Count] : Color4.White;
}
/// <summary> /// <summary>
/// Called when a change is made to the skin. /// Called when a change is made to the skin.
/// </summary> /// </summary>
@ -630,7 +598,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
/// <summary> /// <summary>
/// The time at which state transforms should be applied that line up to <see cref="HitObject"/>'s StartTime. /// The time at which state transforms should be applied that line up to <see cref="HitObject"/>'s StartTime.
/// This is used to offset calls to <see cref="UpdateStateTransforms"/>. /// This is used to offset calls to <see cref="UpdateStartTimeStateTransforms"/>.
/// </summary> /// </summary>
public double StateUpdateTime => HitObject.StartTime; public double StateUpdateTime => HitObject.StartTime;

View File

@ -65,6 +65,10 @@ namespace osu.Game.Scoring.Legacy
scoreInfo.Mods = currentRuleset.ConvertFromLegacyMods((LegacyMods)sr.ReadInt32()).ToArray(); scoreInfo.Mods = currentRuleset.ConvertFromLegacyMods((LegacyMods)sr.ReadInt32()).ToArray();
// lazer replays get a really high version number.
if (version < LegacyScoreEncoder.FIRST_LAZER_VERSION)
scoreInfo.Mods = scoreInfo.Mods.Append(currentRuleset.GetAllMods().OfType<ModClassic>().Single()).ToArray();
currentBeatmap = workingBeatmap.GetPlayableBeatmap(currentRuleset.RulesetInfo, scoreInfo.Mods); currentBeatmap = workingBeatmap.GetPlayableBeatmap(currentRuleset.RulesetInfo, scoreInfo.Mods);
scoreInfo.Beatmap = currentBeatmap.BeatmapInfo; scoreInfo.Beatmap = currentBeatmap.BeatmapInfo;

View File

@ -15,7 +15,16 @@ namespace osu.Game.Scoring.Legacy
{ {
public class LegacyScoreEncoder public class LegacyScoreEncoder
{ {
public const int LATEST_VERSION = 128; /// <summary>
/// Database version in stable-compatible YYYYMMDD format.
/// Should be incremented if any changes are made to the format/usage.
/// </summary>
public const int LATEST_VERSION = FIRST_LAZER_VERSION;
/// <summary>
/// The first stable-compatible YYYYMMDD format version given to lazer usage of replays.
/// </summary>
public const int FIRST_LAZER_VERSION = 30000000;
private readonly Score score; private readonly Score score;
private readonly IBeatmap beatmap; private readonly IBeatmap beatmap;

View File

@ -76,9 +76,6 @@ namespace osu.Game.Scoring
else if (localAPIMods != null) else if (localAPIMods != null)
scoreMods = apiMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); scoreMods = apiMods.Select(m => m.ToMod(rulesetInstance)).ToArray();
if (IsLegacyScore)
scoreMods = scoreMods.Append(rulesetInstance.GetAllMods().OfType<ModClassic>().Single()).ToArray();
return scoreMods; return scoreMods;
} }
set set
@ -201,33 +198,12 @@ namespace osu.Game.Scoring
[JsonProperty("position")] [JsonProperty("position")]
public int? Position { get; set; } public int? Position { get; set; }
private bool isLegacyScore;
/// <summary> /// <summary>
/// Whether this <see cref="ScoreInfo"/> represents a legacy (osu!stable) score. /// Whether this <see cref="ScoreInfo"/> represents a legacy (osu!stable) score.
/// </summary> /// </summary>
[JsonIgnore] [JsonIgnore]
[NotMapped] [NotMapped]
public bool IsLegacyScore public bool IsLegacyScore => Mods.OfType<ModClassic>().Any();
{
get
{
if (isLegacyScore)
return true;
// The above check will catch legacy online scores that have an appropriate UserString + UserId.
// For non-online scores such as those imported in, a heuristic is used based on the following table:
//
// Mode | UserString | UserId
// --------------- | ---------- | ---------
// stable | <username> | 1
// lazer | <username> | <userid>
// lazer (offline) | Guest | 1
return ID > 0 && UserID == 1 && UserString != "Guest";
}
set => isLegacyScore = value;
}
public IEnumerable<HitResultDisplayStatistic> GetStatisticsForDisplay() public IEnumerable<HitResultDisplayStatistic> GetStatisticsForDisplay()
{ {

View File

@ -5,8 +5,8 @@ using System.Threading;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Utils;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Backgrounds;
@ -53,14 +53,41 @@ namespace osu.Game.Screens.Backgrounds
mode.ValueChanged += _ => Next(); mode.ValueChanged += _ => Next();
beatmap.ValueChanged += _ => Next(); beatmap.ValueChanged += _ => Next();
introSequence.ValueChanged += _ => Next(); introSequence.ValueChanged += _ => Next();
seasonalBackgroundLoader.SeasonalBackgroundChanged += Next; seasonalBackgroundLoader.SeasonalBackgroundChanged += () => Next();
currentDisplay = RNG.Next(0, background_count); currentDisplay = RNG.Next(0, background_count);
Next(); Next();
} }
private void display(Background newBackground) private ScheduledDelegate nextTask;
private CancellationTokenSource cancellationTokenSource;
/// <summary>
/// Request loading the next background.
/// </summary>
/// <returns>Whether a new background was queued for load. May return false if the current background is still valid.</returns>
public bool Next()
{
var nextBackground = createBackground();
// in the case that the background hasn't changed, we want to avoid cancelling any tasks that could still be loading.
if (nextBackground == background)
return false;
cancellationTokenSource?.Cancel();
cancellationTokenSource = new CancellationTokenSource();
nextTask?.Cancel();
nextTask = Scheduler.AddDelayed(() =>
{
LoadComponentAsync(nextBackground, displayNext, cancellationTokenSource.Token);
}, 100);
return true;
}
private void displayNext(Background newBackground)
{ {
background?.FadeOut(800, Easing.InOutSine); background?.FadeOut(800, Easing.InOutSine);
background?.Expire(); background?.Expire();
@ -69,82 +96,55 @@ namespace osu.Game.Screens.Backgrounds
currentDisplay++; currentDisplay++;
} }
private ScheduledDelegate nextTask;
private CancellationTokenSource cancellationTokenSource;
public void Next()
{
nextTask?.Cancel();
cancellationTokenSource?.Cancel();
cancellationTokenSource = new CancellationTokenSource();
nextTask = Scheduler.AddDelayed(() => LoadComponentAsync(createBackground(), display, cancellationTokenSource.Token), 100);
}
private Background createBackground() private Background createBackground()
{ {
Background newBackground; // seasonal background loading gets highest priority.
string backgroundName; Background newBackground = seasonalBackgroundLoader.LoadNextBackground();
var seasonalBackground = seasonalBackgroundLoader.LoadNextBackground(); if (newBackground == null && user.Value?.IsSupporter == true)
if (seasonalBackground != null)
{
seasonalBackground.Depth = currentDisplay;
return seasonalBackground;
}
switch (introSequence.Value)
{
case IntroSequence.Welcome:
backgroundName = "Intro/Welcome/menu-background";
break;
default:
backgroundName = $@"Menu/menu-background-{currentDisplay % background_count + 1}";
break;
}
if (user.Value?.IsSupporter ?? false)
{ {
switch (mode.Value) switch (mode.Value)
{ {
case BackgroundSource.Beatmap: case BackgroundSource.Beatmap:
newBackground = new BeatmapBackground(beatmap.Value, backgroundName);
break;
case BackgroundSource.BeatmapWithStoryboard: case BackgroundSource.BeatmapWithStoryboard:
newBackground = AllowStoryboardBackground {
? new BeatmapBackgroundWithStoryboard(beatmap.Value, backgroundName) if (mode.Value == BackgroundSource.BeatmapWithStoryboard && AllowStoryboardBackground)
: new BeatmapBackground(beatmap.Value, backgroundName); newBackground = new BeatmapBackgroundWithStoryboard(beatmap.Value, getBackgroundTextureName());
break; newBackground ??= new BeatmapBackground(beatmap.Value, getBackgroundTextureName());
default: break;
newBackground = new SkinnedBackground(skin.Value, backgroundName); }
case BackgroundSource.Skin:
// default skins should use the default background rotation, which won't be the case if a SkinBackground is created for them.
if (skin.Value is DefaultSkin || skin.Value is DefaultLegacySkin)
break;
newBackground = new SkinBackground(skin.Value, getBackgroundTextureName());
break; break;
} }
} }
else
newBackground = new Background(backgroundName);
// this method is called in many cases where the background might not necessarily need to change.
// if an equivalent background is currently being shown, we don't want to load it again.
if (newBackground?.Equals(background) == true)
return background;
newBackground ??= new Background(getBackgroundTextureName());
newBackground.Depth = currentDisplay; newBackground.Depth = currentDisplay;
return newBackground; return newBackground;
} }
private class SkinnedBackground : Background private string getBackgroundTextureName()
{ {
private readonly Skin skin; switch (introSequence.Value)
public SkinnedBackground(Skin skin, string fallbackTextureName)
: base(fallbackTextureName)
{ {
this.skin = skin; case IntroSequence.Welcome:
} return @"Intro/Welcome/menu-background";
[BackgroundDependencyLoader] default:
private void load() return $@"Menu/menu-background-{currentDisplay % background_count + 1}";
{
Sprite.Texture = skin.GetTexture("menu-background") ?? Sprite.Texture;
} }
} }
} }

View File

@ -67,8 +67,11 @@ namespace osu.Game.Screens
/// Invoked when the back button has been pressed to close any overlays before exiting this <see cref="IOsuScreen"/>. /// Invoked when the back button has been pressed to close any overlays before exiting this <see cref="IOsuScreen"/>.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// If this <see cref="IOsuScreen"/> has not yet finished loading, the exit will occur immediately without this method being invoked.
/// <para>
/// Return <c>true</c> to block this <see cref="IOsuScreen"/> from being exited after closing an overlay. /// Return <c>true</c> to block this <see cref="IOsuScreen"/> from being exited after closing an overlay.
/// Return <c>false</c> if this <see cref="IOsuScreen"/> should continue exiting. /// Return <c>false</c> if this <see cref="IOsuScreen"/> should continue exiting.
/// </para>
/// </remarks> /// </remarks>
bool OnBackButton(); bool OnBackButton();
} }

View File

@ -22,6 +22,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
// Isolates beatmap/ruleset to this screen. // Isolates beatmap/ruleset to this screen.
public override bool DisallowExternalBeatmapRulesetChanges => true; public override bool DisallowExternalBeatmapRulesetChanges => true;
// We are managing our own adjustments. For now, this happens inside the Player instances themselves.
public override bool AllowRateAdjustments => false;
/// <summary> /// <summary>
/// Whether all spectating players have finished loading. /// Whether all spectating players have finished loading.
/// </summary> /// </summary>

View File

@ -253,7 +253,10 @@ namespace osu.Game.Screens.OnlinePlay
public override bool OnBackButton() public override bool OnBackButton()
{ {
if ((screenStack.CurrentScreen as IOnlinePlaySubScreen)?.OnBackButton() == true) if (!(screenStack.CurrentScreen is IOnlinePlaySubScreen onlineSubScreen))
return false;
if (((Drawable)onlineSubScreen).IsLoaded && onlineSubScreen.AllowBackButton && onlineSubScreen.OnBackButton())
return true; return true;
if (screenStack.CurrentScreen != null && !(screenStack.CurrentScreen is LoungeSubScreen)) if (screenStack.CurrentScreen != null && !(screenStack.CurrentScreen is LoungeSubScreen))

View File

@ -214,7 +214,10 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
protected override void OnNewJudgement(JudgementResult judgement) protected override void OnNewJudgement(JudgementResult judgement)
{ {
if (!judgement.IsHit) if (!judgement.IsHit || judgement.HitObject.HitWindows?.WindowFor(HitResult.Miss) == 0)
return;
if (!judgement.Type.IsScorable() || judgement.Type.IsBonus())
return; return;
if (judgementsContainer.Count > max_concurrent_judgements) if (judgementsContainer.Count > max_concurrent_judgements)
@ -244,7 +247,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
private float getRelativeJudgementPosition(double value) => Math.Clamp((float)((value / maxHitWindow) + 1) / 2, 0, 1); private float getRelativeJudgementPosition(double value) => Math.Clamp((float)((value / maxHitWindow) + 1) / 2, 0, 1);
private class JudgementLine : CompositeDrawable internal class JudgementLine : CompositeDrawable
{ {
private const int judgement_fade_duration = 5000; private const int judgement_fade_duration = 5000;

View File

@ -7,6 +7,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -24,7 +25,13 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
InternalChild = judgementsFlow = new JudgementFlow(); InternalChild = judgementsFlow = new JudgementFlow();
} }
protected override void OnNewJudgement(JudgementResult judgement) => judgementsFlow.Push(GetColourForHitResult(judgement.Type)); protected override void OnNewJudgement(JudgementResult judgement)
{
if (!judgement.Type.IsScorable() || judgement.Type.IsBonus())
return;
judgementsFlow.Push(GetColourForHitResult(judgement.Type));
}
private class JudgementFlow : FillFlowContainer<HitErrorCircle> private class JudgementFlow : FillFlowContainer<HitErrorCircle>
{ {
@ -53,7 +60,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
} }
} }
private class HitErrorCircle : Container internal class HitErrorCircle : Container
{ {
public bool IsRemoved { get; private set; } public bool IsRemoved { get; private set; }

View File

@ -32,15 +32,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
{ {
base.LoadComplete(); base.LoadComplete();
processor.NewJudgement += onNewJudgement; processor.NewJudgement += OnNewJudgement;
}
private void onNewJudgement(JudgementResult result)
{
if (result.HitObject.HitWindows?.WindowFor(HitResult.Miss) == 0)
return;
OnNewJudgement(result);
} }
protected abstract void OnNewJudgement(JudgementResult judgement); protected abstract void OnNewJudgement(JudgementResult judgement);
@ -49,6 +41,8 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
{ {
switch (result) switch (result)
{ {
case HitResult.SmallTickMiss:
case HitResult.LargeTickMiss:
case HitResult.Miss: case HitResult.Miss:
return colours.Red; return colours.Red;
@ -61,6 +55,8 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
case HitResult.Good: case HitResult.Good:
return colours.GreenLight; return colours.GreenLight;
case HitResult.SmallTickHit:
case HitResult.LargeTickHit:
case HitResult.Great: case HitResult.Great:
return colours.Blue; return colours.Blue;
@ -74,7 +70,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
base.Dispose(isDisposing); base.Dispose(isDisposing);
if (processor != null) if (processor != null)
processor.NewJudgement -= onNewJudgement; processor.NewJudgement -= OnNewJudgement;
} }
} }
} }

View File

@ -22,6 +22,8 @@ namespace osu.Game.Skinning
{ {
public class DefaultSkin : Skin public class DefaultSkin : Skin
{ {
private readonly IStorageResourceProvider resources;
public DefaultSkin(IStorageResourceProvider resources) public DefaultSkin(IStorageResourceProvider resources)
: this(SkinInfo.Default, resources) : this(SkinInfo.Default, resources)
{ {
@ -31,12 +33,23 @@ namespace osu.Game.Skinning
public DefaultSkin(SkinInfo skin, IStorageResourceProvider resources) public DefaultSkin(SkinInfo skin, IStorageResourceProvider resources)
: base(skin, resources) : base(skin, resources)
{ {
this.resources = resources;
Configuration = new DefaultSkinConfiguration(); Configuration = new DefaultSkinConfiguration();
} }
public override Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => null; public override Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => null;
public override ISample GetSample(ISampleInfo sampleInfo) => null; public override ISample GetSample(ISampleInfo sampleInfo)
{
foreach (var lookup in sampleInfo.LookupNames)
{
var sample = resources.AudioManager.Samples.Get(lookup);
if (sample != null)
return sample;
}
return null;
}
public override Drawable GetDrawableComponent(ISkinComponent component) public override Drawable GetDrawableComponent(ISkinComponent component)
{ {

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using JetBrains.Annotations;
namespace osu.Game.Skinning namespace osu.Game.Skinning
{ {
@ -11,5 +12,13 @@ namespace osu.Game.Skinning
public interface ISkinSource : ISkin public interface ISkinSource : ISkin
{ {
event Action SourceChanged; event Action SourceChanged;
/// <summary>
/// Find the first (if any) skin that can fulfill the lookup.
/// This should be used for cases where subsequent lookups (for related components) need to occur on the same skin.
/// </summary>
/// <returns>The skin to be used for subsequent lookups, or <c>null</c> if none is available.</returns>
[CanBeNull]
ISkin FindProvider(Func<ISkin, bool> lookupFunction);
} }
} }

View File

@ -20,9 +20,6 @@ namespace osu.Game.Skinning
{ {
private const double epic_cutoff = 0.5; private const double epic_cutoff = 0.5;
[Resolved]
private ISkinSource skin { get; set; }
private LegacyHealthPiece fill; private LegacyHealthPiece fill;
private LegacyHealthPiece marker; private LegacyHealthPiece marker;
@ -31,10 +28,13 @@ namespace osu.Game.Skinning
private bool isNewStyle; private bool isNewStyle;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load(ISkinSource source)
{ {
AutoSizeAxes = Axes.Both; AutoSizeAxes = Axes.Both;
var skin = source.FindProvider(s => getTexture(s, "bg") != null);
// the marker lookup to decide which display style must be performed on the source of the bg, which is the most common element.
isNewStyle = getTexture(skin, "marker") != null; isNewStyle = getTexture(skin, "marker") != null;
// background implementation is the same for both versions. // background implementation is the same for both versions.
@ -76,7 +76,7 @@ namespace osu.Game.Skinning
protected override void Flash(JudgementResult result) => marker.Flash(result); protected override void Flash(JudgementResult result) => marker.Flash(result);
private static Texture getTexture(ISkinSource skin, string name) => skin.GetTexture($"scorebar-{name}"); private static Texture getTexture(ISkin skin, string name) => skin?.GetTexture($"scorebar-{name}");
private static Color4 getFillColour(double hp) private static Color4 getFillColour(double hp)
{ {
@ -95,7 +95,7 @@ namespace osu.Game.Skinning
private readonly Texture dangerTexture; private readonly Texture dangerTexture;
private readonly Texture superDangerTexture; private readonly Texture superDangerTexture;
public LegacyOldStyleMarker(ISkinSource skin) public LegacyOldStyleMarker(ISkin skin)
{ {
normalTexture = getTexture(skin, "ki"); normalTexture = getTexture(skin, "ki");
dangerTexture = getTexture(skin, "kidanger"); dangerTexture = getTexture(skin, "kidanger");
@ -126,9 +126,9 @@ namespace osu.Game.Skinning
public class LegacyNewStyleMarker : LegacyMarker public class LegacyNewStyleMarker : LegacyMarker
{ {
private readonly ISkinSource skin; private readonly ISkin skin;
public LegacyNewStyleMarker(ISkinSource skin) public LegacyNewStyleMarker(ISkin skin)
{ {
this.skin = skin; this.skin = skin;
} }
@ -150,7 +150,7 @@ namespace osu.Game.Skinning
internal class LegacyOldStyleFill : LegacyHealthPiece internal class LegacyOldStyleFill : LegacyHealthPiece
{ {
public LegacyOldStyleFill(ISkinSource skin) public LegacyOldStyleFill(ISkin skin)
{ {
// required for sizing correctly.. // required for sizing correctly..
var firstFrame = getTexture(skin, "colour-0"); var firstFrame = getTexture(skin, "colour-0");
@ -173,7 +173,7 @@ namespace osu.Game.Skinning
internal class LegacyNewStyleFill : LegacyHealthPiece internal class LegacyNewStyleFill : LegacyHealthPiece
{ {
public LegacyNewStyleFill(ISkinSource skin) public LegacyNewStyleFill(ISkin skin)
{ {
InternalChild = new Sprite InternalChild = new Sprite
{ {

View File

@ -60,10 +60,17 @@ namespace osu.Game.Skinning
{ {
} }
protected LegacySkin(SkinInfo skin, [CanBeNull] IResourceStore<byte[]> storage, [CanBeNull] IStorageResourceProvider resources, string filename) /// <summary>
/// Construct a new legacy skin instance.
/// </summary>
/// <param name="skin">The model for this skin.</param>
/// <param name="storage">A storage for looking up files within this skin using user-facing filenames.</param>
/// <param name="resources">Access to raw game resources.</param>
/// <param name="configurationFilename">The user-facing filename of the configuration file to be parsed. Can accept an .osu or skin.ini file.</param>
protected LegacySkin(SkinInfo skin, [CanBeNull] IResourceStore<byte[]> storage, [CanBeNull] IStorageResourceProvider resources, string configurationFilename)
: base(skin, resources) : base(skin, resources)
{ {
using (var stream = storage?.GetStream(filename)) using (var stream = storage?.GetStream(configurationFilename))
{ {
if (stream != null) if (stream != null)
{ {
@ -127,7 +134,7 @@ namespace osu.Game.Skinning
case LegacyManiaSkinConfigurationLookup maniaLookup: case LegacyManiaSkinConfigurationLookup maniaLookup:
if (!AllowManiaSkin) if (!AllowManiaSkin)
return null; break;
var result = lookupForMania<TValue>(maniaLookup); var result = lookupForMania<TValue>(maniaLookup);
if (result != null) if (result != null)
@ -390,6 +397,7 @@ namespace osu.Game.Skinning
return null; return null;
case GameplaySkinComponent<HitResult> resultComponent: case GameplaySkinComponent<HitResult> resultComponent:
// TODO: this should be inside the judgement pieces.
Func<Drawable> createDrawable = () => getJudgementAnimation(resultComponent.Component); Func<Drawable> createDrawable = () => getJudgementAnimation(resultComponent.Component);
// kind of wasteful that we throw this away, but should do for now. // kind of wasteful that we throw this away, but should do for now.
@ -485,7 +493,9 @@ namespace osu.Game.Skinning
var sample = Samples?.Get(lookup); var sample = Samples?.Get(lookup);
if (sample != null) if (sample != null)
{
return sample; return sample;
}
} }
return null; return null;
@ -518,8 +528,7 @@ namespace osu.Game.Skinning
yield return componentName; yield return componentName;
// Fall back to using the last piece for components coming from lazer (e.g. "Gameplay/osu/approachcircle" -> "approachcircle"). // Fall back to using the last piece for components coming from lazer (e.g. "Gameplay/osu/approachcircle" -> "approachcircle").
string lastPiece = componentName.Split('/').Last(); yield return componentName.Split('/').Last();
yield return componentName.StartsWith("Gameplay/taiko/", StringComparison.Ordinal) ? "taiko-" + lastPiece : lastPiece;
} }
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)

View File

@ -27,6 +27,18 @@ namespace osu.Game.Skinning
{ {
Texture texture; Texture texture;
// find the first source which provides either the animated or non-animated version.
ISkin skin = (source as ISkinSource)?.FindProvider(s =>
{
if (animatable && s.GetTexture(getFrameName(0)) != null)
return true;
return s.GetTexture(componentName, wrapModeS, wrapModeT) != null;
}) ?? source;
if (skin == null)
return null;
if (animatable) if (animatable)
{ {
var textures = getTextures().ToArray(); var textures = getTextures().ToArray();
@ -35,7 +47,7 @@ namespace osu.Game.Skinning
{ {
var animation = new SkinnableTextureAnimation(startAtCurrentTime) var animation = new SkinnableTextureAnimation(startAtCurrentTime)
{ {
DefaultFrameLength = frameLength ?? getFrameLength(source, applyConfigFrameRate, textures), DefaultFrameLength = frameLength ?? getFrameLength(skin, applyConfigFrameRate, textures),
Loop = looping, Loop = looping,
}; };
@ -47,7 +59,7 @@ namespace osu.Game.Skinning
} }
// if an animation was not allowed or not found, fall back to a sprite retrieval. // if an animation was not allowed or not found, fall back to a sprite retrieval.
if ((texture = source.GetTexture(componentName, wrapModeS, wrapModeT)) != null) if ((texture = skin.GetTexture(componentName, wrapModeS, wrapModeT)) != null)
return new Sprite { Texture = texture }; return new Sprite { Texture = texture };
return null; return null;
@ -56,12 +68,14 @@ namespace osu.Game.Skinning
{ {
for (int i = 0; true; i++) for (int i = 0; true; i++)
{ {
if ((texture = source.GetTexture($"{componentName}{animationSeparator}{i}", wrapModeS, wrapModeT)) == null) if ((texture = skin.GetTexture(getFrameName(i), wrapModeS, wrapModeT)) == null)
break; break;
yield return texture; yield return texture;
} }
} }
string getFrameName(int frameIndex) => $"{componentName}{animationSeparator}{frameIndex}";
} }
public static bool HasFont(this ISkin source, LegacyFont font) public static bool HasFont(this ISkin source, LegacyFont font)

View File

@ -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 osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -15,7 +16,7 @@ namespace osu.Game.Skinning
/// <summary> /// <summary>
/// Transformer used to handle support of legacy features for individual rulesets. /// Transformer used to handle support of legacy features for individual rulesets.
/// </summary> /// </summary>
public abstract class LegacySkinTransformer : ISkin public abstract class LegacySkinTransformer : ISkinSource
{ {
/// <summary> /// <summary>
/// Source of the <see cref="ISkin"/> which is being transformed. /// Source of the <see cref="ISkin"/> which is being transformed.
@ -47,5 +48,13 @@ namespace osu.Game.Skinning
} }
public abstract IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup); public abstract IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup);
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => Source.FindProvider(lookupFunction);
public event Action SourceChanged
{
add { throw new NotSupportedException(); }
remove { }
}
} }
} }

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
@ -70,33 +71,50 @@ namespace osu.Game.Skinning
updateSample(); updateSample();
} }
protected override void SkinChanged(ISkinSource skin, bool allowFallback) protected override void LoadComplete()
{ {
base.SkinChanged(skin, allowFallback); base.LoadComplete();
CurrentSkin.SourceChanged += skinChangedImmediate;
}
private void skinChangedImmediate()
{
// Clean up the previous sample immediately on a source change.
// This avoids a potential call to Play() of an already disposed sample (samples are disposed along with the skin, but SkinChanged is scheduled).
clearPreviousSamples();
}
protected override void SkinChanged(ISkinSource skin)
{
base.SkinChanged(skin);
updateSample(); updateSample();
} }
/// <summary>
/// Whether this sample was playing before a skin source change.
/// </summary>
private bool wasPlaying;
private void clearPreviousSamples()
{
// only run if the samples aren't already cleared.
// this ensures the "wasPlaying" state is stored correctly even if multiple clear calls are executed.
if (!sampleContainer.Any()) return;
wasPlaying = Playing;
sampleContainer.Clear();
Sample = null;
}
private void updateSample() private void updateSample()
{ {
if (sampleInfo == null) if (sampleInfo == null)
return; return;
bool wasPlaying = Playing;
sampleContainer.Clear();
Sample = null;
var sample = CurrentSkin.GetSample(sampleInfo); var sample = CurrentSkin.GetSample(sampleInfo);
if (sample == null && AllowDefaultFallback)
{
foreach (var lookup in sampleInfo.LookupNames)
{
if ((sample = sampleStore.Get(lookup)) != null)
break;
}
}
if (sample == null) if (sample == null)
return; return;
@ -155,6 +173,14 @@ namespace osu.Game.Skinning
} }
} }
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (CurrentSkin != null)
CurrentSkin.SourceChanged -= skinChangedImmediate;
}
#region Re-expose AudioContainer #region Re-expose AudioContainer
public BindableNumber<double> Volume => sampleContainer.Volume; public BindableNumber<double> Volume => sampleContainer.Volume;

View File

@ -39,7 +39,7 @@ namespace osu.Game.Skinning
private readonly IResourceStore<byte[]> resources; private readonly IResourceStore<byte[]> resources;
public readonly Bindable<Skin> CurrentSkin = new Bindable<Skin>(new DefaultSkin(null)); public readonly Bindable<Skin> CurrentSkin = new Bindable<Skin>();
public readonly Bindable<SkinInfo> CurrentSkinInfo = new Bindable<SkinInfo>(SkinInfo.Default) { Default = SkinInfo.Default }; public readonly Bindable<SkinInfo> CurrentSkinInfo = new Bindable<SkinInfo>(SkinInfo.Default) { Default = SkinInfo.Default };
public override IEnumerable<string> HandledExtensions => new[] { ".osk" }; public override IEnumerable<string> HandledExtensions => new[] { ".osk" };
@ -48,6 +48,8 @@ namespace osu.Game.Skinning
protected override string ImportFromStablePath => "Skins"; protected override string ImportFromStablePath => "Skins";
private readonly Skin defaultLegacySkin;
public SkinManager(Storage storage, DatabaseContextFactory contextFactory, GameHost host, IResourceStore<byte[]> resources, AudioManager audio) public SkinManager(Storage storage, DatabaseContextFactory contextFactory, GameHost host, IResourceStore<byte[]> resources, AudioManager audio)
: base(storage, contextFactory, new SkinStore(contextFactory, storage), host) : base(storage, contextFactory, new SkinStore(contextFactory, storage), host)
{ {
@ -55,7 +57,11 @@ namespace osu.Game.Skinning
this.host = host; this.host = host;
this.resources = resources; this.resources = resources;
defaultLegacySkin = new DefaultLegacySkin(this);
CurrentSkinInfo.ValueChanged += skin => CurrentSkin.Value = GetSkin(skin.NewValue); CurrentSkinInfo.ValueChanged += skin => CurrentSkin.Value = GetSkin(skin.NewValue);
CurrentSkin.Value = new DefaultSkin(this);
CurrentSkin.ValueChanged += skin => CurrentSkin.ValueChanged += skin =>
{ {
if (skin.NewValue.SkinInfo != CurrentSkinInfo.Value) if (skin.NewValue.SkinInfo != CurrentSkinInfo.Value)
@ -204,13 +210,41 @@ namespace osu.Game.Skinning
public event Action SourceChanged; public event Action SourceChanged;
public Drawable GetDrawableComponent(ISkinComponent component) => CurrentSkin.Value.GetDrawableComponent(component); public Drawable GetDrawableComponent(ISkinComponent component) => lookupWithFallback(s => s.GetDrawableComponent(component));
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => CurrentSkin.Value.GetTexture(componentName, wrapModeS, wrapModeT); public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => lookupWithFallback(s => s.GetTexture(componentName, wrapModeS, wrapModeT));
public ISample GetSample(ISampleInfo sampleInfo) => CurrentSkin.Value.GetSample(sampleInfo); public ISample GetSample(ISampleInfo sampleInfo) => lookupWithFallback(s => s.GetSample(sampleInfo));
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => CurrentSkin.Value.GetConfig<TLookup, TValue>(lookup); public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => lookupWithFallback(s => s.GetConfig<TLookup, TValue>(lookup));
public ISkin FindProvider(Func<ISkin, bool> lookupFunction)
{
if (lookupFunction(CurrentSkin.Value))
return CurrentSkin.Value;
if (CurrentSkin.Value is LegacySkin && lookupFunction(defaultLegacySkin))
return defaultLegacySkin;
return null;
}
private T lookupWithFallback<T>(Func<ISkin, T> func)
where T : class
{
var selectedSkin = func(CurrentSkin.Value);
if (selectedSkin != null)
return selectedSkin;
// TODO: we also want to return a DefaultLegacySkin here if the current *beatmap* is providing any skinned elements.
// When attempting to address this, we may want to move the full DefaultLegacySkin fallback logic to within Player itself (to better allow
// for beatmap skin visibility).
if (CurrentSkin.Value is LegacySkin)
return func(defaultLegacySkin);
return null;
}
#region IResourceStorageProvider #region IResourceStorageProvider

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -20,8 +21,10 @@ namespace osu.Game.Skinning
{ {
public event Action SourceChanged; public event Action SourceChanged;
[CanBeNull]
private readonly ISkin skin; private readonly ISkin skin;
[CanBeNull]
private ISkinSource fallbackSource; private ISkinSource fallbackSource;
protected virtual bool AllowDrawableLookup(ISkinComponent component) => true; protected virtual bool AllowDrawableLookup(ISkinComponent component) => true;
@ -41,6 +44,22 @@ namespace osu.Game.Skinning
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
} }
public ISkin FindProvider(Func<ISkin, bool> lookupFunction)
{
if (skin is ISkinSource source)
{
if (source.FindProvider(lookupFunction) is ISkin found)
return found;
}
else if (skin != null)
{
if (lookupFunction(skin))
return skin;
}
return fallbackSource?.FindProvider(lookupFunction);
}
public Drawable GetDrawableComponent(ISkinComponent component) public Drawable GetDrawableComponent(ISkinComponent component)
{ {
Drawable sourceDrawable; Drawable sourceDrawable;
@ -85,7 +104,7 @@ namespace osu.Game.Skinning
{ {
if (canUseSkinLookup) if (canUseSkinLookup)
{ {
var bindable = skin.GetConfig<TLookup, TValue>(lookup); var bindable = skin?.GetConfig<TLookup, TValue>(lookup);
if (bindable != null) if (bindable != null)
return bindable; return bindable;
} }

View File

@ -22,22 +22,6 @@ namespace osu.Game.Skinning
/// </summary> /// </summary>
protected ISkinSource CurrentSkin { get; private set; } protected ISkinSource CurrentSkin { get; private set; }
private readonly Func<ISkinSource, bool> allowFallback;
/// <summary>
/// Whether fallback to default skin should be allowed if the custom skin is missing this resource.
/// </summary>
protected bool AllowDefaultFallback => allowFallback == null || allowFallback.Invoke(CurrentSkin);
/// <summary>
/// Create a new <see cref="SkinReloadableDrawable"/>
/// </summary>
/// <param name="allowFallback">A conditional to decide whether to allow fallback to the default implementation if a skinned element is not present.</param>
protected SkinReloadableDrawable(Func<ISkinSource, bool> allowFallback = null)
{
this.allowFallback = allowFallback;
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(ISkinSource source) private void load(ISkinSource source)
{ {
@ -58,7 +42,7 @@ namespace osu.Game.Skinning
private void skinChanged() private void skinChanged()
{ {
SkinChanged(CurrentSkin, AllowDefaultFallback); SkinChanged(CurrentSkin);
OnSkinChanged?.Invoke(); OnSkinChanged?.Invoke();
} }
@ -66,8 +50,7 @@ namespace osu.Game.Skinning
/// Called when a change is made to the skin. /// Called when a change is made to the skin.
/// </summary> /// </summary>
/// <param name="skin">The new skin.</param> /// <param name="skin">The new skin.</param>
/// <param name="allowFallback">Whether fallback to default skin should be allowed if the custom skin is missing this resource.</param> protected virtual void SkinChanged(ISkinSource skin)
protected virtual void SkinChanged(ISkinSource skin, bool allowFallback)
{ {
} }

View File

@ -40,17 +40,14 @@ namespace osu.Game.Skinning
/// </summary> /// </summary>
/// <param name="component">The namespace-complete resource name for this skinnable element.</param> /// <param name="component">The namespace-complete resource name for this skinnable element.</param>
/// <param name="defaultImplementation">A function to create the default skin implementation of this element.</param> /// <param name="defaultImplementation">A function to create the default skin implementation of this element.</param>
/// <param name="allowFallback">A conditional to decide whether to allow fallback to the default implementation if a skinned element is not present.</param>
/// <param name="confineMode">How (if at all) the <see cref="Drawable"/> should be resize to fit within our own bounds.</param> /// <param name="confineMode">How (if at all) the <see cref="Drawable"/> should be resize to fit within our own bounds.</param>
public SkinnableDrawable(ISkinComponent component, Func<ISkinComponent, Drawable> defaultImplementation = null, Func<ISkinSource, bool> allowFallback = null, public SkinnableDrawable(ISkinComponent component, Func<ISkinComponent, Drawable> defaultImplementation = null, ConfineMode confineMode = ConfineMode.NoScaling)
ConfineMode confineMode = ConfineMode.NoScaling) : this(component, confineMode)
: this(component, allowFallback, confineMode)
{ {
createDefault = defaultImplementation; createDefault = defaultImplementation;
} }
protected SkinnableDrawable(ISkinComponent component, Func<ISkinSource, bool> allowFallback = null, ConfineMode confineMode = ConfineMode.NoScaling) protected SkinnableDrawable(ISkinComponent component, ConfineMode confineMode = ConfineMode.NoScaling)
: base(allowFallback)
{ {
this.component = component; this.component = component;
this.confineMode = confineMode; this.confineMode = confineMode;
@ -76,13 +73,13 @@ namespace osu.Game.Skinning
/// </summary> /// </summary>
protected virtual bool ApplySizeRestrictionsToDefault => false; protected virtual bool ApplySizeRestrictionsToDefault => false;
protected override void SkinChanged(ISkinSource skin, bool allowFallback) protected override void SkinChanged(ISkinSource skin)
{ {
Drawable = skin.GetDrawableComponent(component); Drawable = skin.GetDrawableComponent(component);
isDefault = false; isDefault = false;
if (Drawable == null && allowFallback) if (Drawable == null)
{ {
Drawable = CreateDefault(component); Drawable = CreateDefault(component);
isDefault = true; isDefault = true;

View File

@ -1,7 +1,6 @@
// 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 osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
@ -19,8 +18,8 @@ namespace osu.Game.Skinning
[Resolved] [Resolved]
private TextureStore textures { get; set; } private TextureStore textures { get; set; }
public SkinnableSprite(string textureName, Func<ISkinSource, bool> allowFallback = null, ConfineMode confineMode = ConfineMode.NoScaling) public SkinnableSprite(string textureName, ConfineMode confineMode = ConfineMode.NoScaling)
: base(new SpriteComponent(textureName), allowFallback, confineMode) : base(new SpriteComponent(textureName), confineMode)
{ {
} }

View File

@ -9,14 +9,14 @@ namespace osu.Game.Skinning
{ {
public class SkinnableSpriteText : SkinnableDrawable, IHasText public class SkinnableSpriteText : SkinnableDrawable, IHasText
{ {
public SkinnableSpriteText(ISkinComponent component, Func<ISkinComponent, SpriteText> defaultImplementation, Func<ISkinSource, bool> allowFallback = null, ConfineMode confineMode = ConfineMode.NoScaling) public SkinnableSpriteText(ISkinComponent component, Func<ISkinComponent, SpriteText> defaultImplementation, ConfineMode confineMode = ConfineMode.NoScaling)
: base(component, defaultImplementation, allowFallback, confineMode) : base(component, defaultImplementation, confineMode)
{ {
} }
protected override void SkinChanged(ISkinSource skin, bool allowFallback) protected override void SkinChanged(ISkinSource skin)
{ {
base.SkinChanged(skin, allowFallback); base.SkinChanged(skin);
if (Drawable is IHasText textDrawable) if (Drawable is IHasText textDrawable)
textDrawable.Text = Text; textDrawable.Text = Text;

View File

@ -73,9 +73,9 @@ namespace osu.Game.Skinning
components.Remove(component); components.Remove(component);
} }
protected override void SkinChanged(ISkinSource skin, bool allowFallback) protected override void SkinChanged(ISkinSource skin)
{ {
base.SkinChanged(skin, allowFallback); base.SkinChanged(skin);
Reload(); Reload();
} }

View File

@ -31,9 +31,9 @@ namespace osu.Game.Storyboards.Drawables
[Resolved] [Resolved]
private IBindable<IReadOnlyList<Mod>> mods { get; set; } private IBindable<IReadOnlyList<Mod>> mods { get; set; }
protected override void SkinChanged(ISkinSource skin, bool allowFallback) protected override void SkinChanged(ISkinSource skin)
{ {
base.SkinChanged(skin, allowFallback); base.SkinChanged(skin);
foreach (var mod in mods.Value.OfType<IApplicableToSample>()) foreach (var mod in mods.Value.OfType<IApplicableToSample>())
{ {

View File

@ -158,6 +158,8 @@ namespace osu.Game.Tests.Beatmaps
add { } add { }
remove { } remove { }
} }
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => null;
} }
} }
} }

View File

@ -34,7 +34,7 @@
<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="ppy.osu.Framework" Version="2021.607.0" /> <PackageReference Include="ppy.osu.Framework" Version="2021.608.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.525.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.525.0" />
<PackageReference Include="Sentry" Version="3.4.0" /> <PackageReference Include="Sentry" Version="3.4.0" />
<PackageReference Include="SharpCompress" Version="0.28.2" /> <PackageReference Include="SharpCompress" Version="0.28.2" />

View File

@ -70,7 +70,7 @@
<Reference Include="System.Net.Http" /> <Reference Include="System.Net.Http" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.607.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2021.608.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.525.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.525.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) -->
@ -93,7 +93,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="ppy.osu.Framework" Version="2021.607.0" /> <PackageReference Include="ppy.osu.Framework" Version="2021.608.0" />
<PackageReference Include="SharpCompress" Version="0.28.2" /> <PackageReference Include="SharpCompress" Version="0.28.2" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="SharpRaven" Version="2.4.0" /> <PackageReference Include="SharpRaven" Version="2.4.0" />