Merge pull request #12683 from frenzibyte/legacy-beatmap-combo-offset

Apply combo offsets "colour hax" only on beatmap skins
This commit is contained in:
Dean Herbert 2021-07-23 14:30:18 +09:00 committed by GitHub
commit 50a2abbe7f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 177 additions and 34 deletions

View File

@ -95,6 +95,14 @@ namespace osu.Game.Rulesets.Catch.Objects
set => ComboIndexBindable.Value = value; set => ComboIndexBindable.Value = value;
} }
public Bindable<int> ComboIndexWithOffsetsBindable { get; } = new Bindable<int>();
public int ComboIndexWithOffsets
{
get => ComboIndexWithOffsetsBindable.Value;
set => ComboIndexWithOffsetsBindable.Value = value;
}
public Bindable<bool> LastInComboBindable { get; } = new Bindable<bool>(); public Bindable<bool> LastInComboBindable { get; } = new Bindable<bool>();
/// <summary> /// <summary>

View File

@ -1,16 +1,21 @@
// 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.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Skinning; using osu.Game.Skinning;
using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Beatmaps;
using osuTK; using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Tests namespace osu.Game.Rulesets.Osu.Tests
{ {
@ -77,23 +82,106 @@ namespace osu.Game.Rulesets.Osu.Tests
AddAssert("is custom user skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(TestSkin.Colours)); AddAssert("is custom user skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(TestSkin.Colours));
} }
[TestCase(true, true)]
[TestCase(true, false)]
[TestCase(false, true)]
[TestCase(false, false)]
public void TestComboOffsetWithBeatmapColours(bool userHasCustomColours, bool useBeatmapSkin)
{
PrepareBeatmap(() => new OsuCustomSkinWorkingBeatmap(audio, true, getHitCirclesWithLegacyOffsets()));
ConfigureTest(useBeatmapSkin, true, userHasCustomColours);
assertCorrectObjectComboColours("is beatmap skin colours with combo offsets applied",
TestBeatmapSkin.Colours,
(i, obj) => i + 1 + obj.ComboOffset);
}
[TestCase(true)]
[TestCase(false)]
public void TestComboOffsetWithIgnoredBeatmapColours(bool useBeatmapSkin)
{
PrepareBeatmap(() => new OsuCustomSkinWorkingBeatmap(audio, true, getHitCirclesWithLegacyOffsets()));
ConfigureTest(useBeatmapSkin, false, true);
assertCorrectObjectComboColours("is user skin colours without combo offsets applied",
TestSkin.Colours,
(i, _) => i + 1);
}
private void assertCorrectObjectComboColours(string description, Color4[] expectedColours, Func<int, OsuHitObject, int> nextExpectedComboIndex)
{
AddUntilStep("wait for objects to become alive", () =>
TestPlayer.DrawableRuleset.Playfield.AllHitObjects.Count() == TestPlayer.DrawableRuleset.Objects.Count());
AddAssert(description, () =>
{
int index = 0;
return TestPlayer.DrawableRuleset.Playfield.AllHitObjects.All(d =>
{
index = nextExpectedComboIndex(index, (OsuHitObject)d.HitObject);
return checkComboColour(d, expectedColours[index % expectedColours.Length]);
});
});
static bool checkComboColour(DrawableHitObject drawableHitObject, Color4 expectedColour)
{
return drawableHitObject.AccentColour.Value == expectedColour &&
drawableHitObject.NestedHitObjects.All(n => checkComboColour(n, expectedColour));
}
}
private static IEnumerable<OsuHitObject> getHitCirclesWithLegacyOffsets()
{
var hitObjects = new List<OsuHitObject>();
for (int i = 0; i < 10; i++)
{
var hitObject = i % 2 == 0
? (OsuHitObject)new HitCircle()
: new Slider
{
Path = new SliderPath(new[]
{
new PathControlPoint(new Vector2(0, 0)),
new PathControlPoint(new Vector2(100, 0)),
})
};
hitObject.StartTime = i;
hitObject.Position = new Vector2(256, 192);
hitObject.NewCombo = true;
hitObject.ComboOffset = i;
hitObjects.Add(hitObject);
}
return hitObjects;
}
private class OsuCustomSkinWorkingBeatmap : CustomSkinWorkingBeatmap private class OsuCustomSkinWorkingBeatmap : CustomSkinWorkingBeatmap
{ {
public OsuCustomSkinWorkingBeatmap(AudioManager audio, bool hasColours) public OsuCustomSkinWorkingBeatmap(AudioManager audio, bool hasColours, IEnumerable<OsuHitObject> hitObjects = null)
: base(createBeatmap(), audio, hasColours) : base(createBeatmap(hitObjects), audio, hasColours)
{ {
} }
private static IBeatmap createBeatmap() => private static IBeatmap createBeatmap(IEnumerable<OsuHitObject> hitObjects)
new Beatmap {
var beatmap = new Beatmap
{ {
BeatmapInfo = BeatmapInfo =
{ {
BeatmapSet = new BeatmapSetInfo(), BeatmapSet = new BeatmapSetInfo(),
Ruleset = new OsuRuleset().RulesetInfo, Ruleset = new OsuRuleset().RulesetInfo,
}, },
HitObjects = { new HitCircle { Position = new Vector2(256, 192) } }
}; };
beatmap.HitObjects.AddRange(hitObjects ?? new[]
{
new HitCircle { Position = new Vector2(256, 192) }
});
return beatmap;
}
} }
} }
} }

View File

@ -97,6 +97,14 @@ namespace osu.Game.Rulesets.Osu.Objects
set => ComboIndexBindable.Value = value; set => ComboIndexBindable.Value = value;
} }
public Bindable<int> ComboIndexWithOffsetsBindable { get; } = new Bindable<int>();
public int ComboIndexWithOffsets
{
get => ComboIndexWithOffsetsBindable.Value;
set => ComboIndexWithOffsetsBindable.Value = value;
}
public Bindable<bool> LastInComboBindable { get; } = new Bindable<bool>(); public Bindable<bool> LastInComboBindable { get; } = new Bindable<bool>();
public bool LastInCombo public bool LastInCombo

View File

@ -323,12 +323,12 @@ namespace osu.Game.Tests.Beatmaps.Formats
new OsuBeatmapProcessor(converted).PreProcess(); new OsuBeatmapProcessor(converted).PreProcess();
new OsuBeatmapProcessor(converted).PostProcess(); new OsuBeatmapProcessor(converted).PostProcess();
Assert.AreEqual(4, ((IHasComboInformation)converted.HitObjects.ElementAt(0)).ComboIndex); Assert.AreEqual(4, ((IHasComboInformation)converted.HitObjects.ElementAt(0)).ComboIndexWithOffsets);
Assert.AreEqual(5, ((IHasComboInformation)converted.HitObjects.ElementAt(2)).ComboIndex); Assert.AreEqual(5, ((IHasComboInformation)converted.HitObjects.ElementAt(2)).ComboIndexWithOffsets);
Assert.AreEqual(5, ((IHasComboInformation)converted.HitObjects.ElementAt(4)).ComboIndex); Assert.AreEqual(5, ((IHasComboInformation)converted.HitObjects.ElementAt(4)).ComboIndexWithOffsets);
Assert.AreEqual(6, ((IHasComboInformation)converted.HitObjects.ElementAt(6)).ComboIndex); Assert.AreEqual(6, ((IHasComboInformation)converted.HitObjects.ElementAt(6)).ComboIndexWithOffsets);
Assert.AreEqual(11, ((IHasComboInformation)converted.HitObjects.ElementAt(8)).ComboIndex); Assert.AreEqual(11, ((IHasComboInformation)converted.HitObjects.ElementAt(8)).ComboIndexWithOffsets);
Assert.AreEqual(14, ((IHasComboInformation)converted.HitObjects.ElementAt(11)).ComboIndex); Assert.AreEqual(14, ((IHasComboInformation)converted.HitObjects.ElementAt(11)).ComboIndexWithOffsets);
} }
} }
@ -346,12 +346,12 @@ namespace osu.Game.Tests.Beatmaps.Formats
new CatchBeatmapProcessor(converted).PreProcess(); new CatchBeatmapProcessor(converted).PreProcess();
new CatchBeatmapProcessor(converted).PostProcess(); new CatchBeatmapProcessor(converted).PostProcess();
Assert.AreEqual(4, ((IHasComboInformation)converted.HitObjects.ElementAt(0)).ComboIndex); Assert.AreEqual(4, ((IHasComboInformation)converted.HitObjects.ElementAt(0)).ComboIndexWithOffsets);
Assert.AreEqual(5, ((IHasComboInformation)converted.HitObjects.ElementAt(2)).ComboIndex); Assert.AreEqual(5, ((IHasComboInformation)converted.HitObjects.ElementAt(2)).ComboIndexWithOffsets);
Assert.AreEqual(5, ((IHasComboInformation)converted.HitObjects.ElementAt(4)).ComboIndex); Assert.AreEqual(5, ((IHasComboInformation)converted.HitObjects.ElementAt(4)).ComboIndexWithOffsets);
Assert.AreEqual(6, ((IHasComboInformation)converted.HitObjects.ElementAt(6)).ComboIndex); Assert.AreEqual(6, ((IHasComboInformation)converted.HitObjects.ElementAt(6)).ComboIndexWithOffsets);
Assert.AreEqual(11, ((IHasComboInformation)converted.HitObjects.ElementAt(8)).ComboIndex); Assert.AreEqual(11, ((IHasComboInformation)converted.HitObjects.ElementAt(8)).ComboIndexWithOffsets);
Assert.AreEqual(14, ((IHasComboInformation)converted.HitObjects.ElementAt(11)).ComboIndex); Assert.AreEqual(14, ((IHasComboInformation)converted.HitObjects.ElementAt(11)).ComboIndexWithOffsets);
} }
} }

View File

@ -100,6 +100,14 @@ namespace osu.Game.Tests.Gameplay
set => ComboIndexBindable.Value = value; set => ComboIndexBindable.Value = value;
} }
public Bindable<int> ComboIndexWithOffsetsBindable { get; } = new Bindable<int>();
public int ComboIndexWithOffsets
{
get => ComboIndexWithOffsetsBindable.Value;
set => ComboIndexWithOffsetsBindable.Value = value;
}
public Bindable<bool> LastInComboBindable { get; } = new Bindable<bool>(); public Bindable<bool> LastInComboBindable { get; } = new Bindable<bool>();
public bool LastInCombo public bool LastInCombo

View File

@ -34,19 +34,19 @@ namespace osu.Game.Beatmaps
isFirst = false; isFirst = false;
} }
obj.ComboIndex = lastObj?.ComboIndex ?? 0;
obj.ComboIndexWithOffsets = lastObj?.ComboIndexWithOffsets ?? 0;
obj.IndexInCurrentCombo = (lastObj?.IndexInCurrentCombo + 1) ?? 0;
if (obj.NewCombo) if (obj.NewCombo)
{ {
obj.IndexInCurrentCombo = 0; obj.IndexInCurrentCombo = 0;
obj.ComboIndex = (lastObj?.ComboIndex ?? 0) + obj.ComboOffset + 1; obj.ComboIndex++;
obj.ComboIndexWithOffsets += obj.ComboOffset + 1;
if (lastObj != null) if (lastObj != null)
lastObj.LastInCombo = true; lastObj.LastInCombo = true;
} }
else if (lastObj != null)
{
obj.IndexInCurrentCombo = lastObj.IndexInCurrentCombo + 1;
obj.ComboIndex = lastObj.ComboIndex;
}
lastObj = obj; lastObj = obj;
} }

View File

@ -124,7 +124,9 @@ namespace osu.Game.Rulesets.Objects.Drawables
public readonly Bindable<double> StartTimeBindable = new Bindable<double>(); public readonly Bindable<double> StartTimeBindable = new Bindable<double>();
private readonly BindableList<HitSampleInfo> samplesBindable = new BindableList<HitSampleInfo>(); private readonly BindableList<HitSampleInfo> samplesBindable = new BindableList<HitSampleInfo>();
private readonly Bindable<bool> userPositionalHitSounds = new Bindable<bool>(); private readonly Bindable<bool> userPositionalHitSounds = new Bindable<bool>();
private readonly Bindable<int> comboIndexBindable = new Bindable<int>(); private readonly Bindable<int> comboIndexBindable = new Bindable<int>();
private readonly Bindable<int> comboIndexWithOffsetsBindable = new Bindable<int>();
protected override bool RequiresChildrenUpdate => true; protected override bool RequiresChildrenUpdate => true;
@ -185,7 +187,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
{ {
base.LoadComplete(); base.LoadComplete();
comboIndexBindable.BindValueChanged(_ => UpdateComboColour(), true); comboIndexBindable.BindValueChanged(_ => UpdateComboColour());
comboIndexWithOffsetsBindable.BindValueChanged(_ => UpdateComboColour(), true);
updateState(ArmedState.Idle, true); updateState(ArmedState.Idle, true);
} }
@ -250,7 +253,10 @@ namespace osu.Game.Rulesets.Objects.Drawables
StartTimeBindable.BindValueChanged(onStartTimeChanged); StartTimeBindable.BindValueChanged(onStartTimeChanged);
if (HitObject is IHasComboInformation combo) if (HitObject is IHasComboInformation combo)
{
comboIndexBindable.BindTo(combo.ComboIndexBindable); comboIndexBindable.BindTo(combo.ComboIndexBindable);
comboIndexWithOffsetsBindable.BindTo(combo.ComboIndexWithOffsetsBindable);
}
samplesBindable.BindTo(HitObject.SamplesBindable); samplesBindable.BindTo(HitObject.SamplesBindable);
samplesBindable.BindCollectionChanged(onSamplesChanged, true); samplesBindable.BindCollectionChanged(onSamplesChanged, true);
@ -275,8 +281,13 @@ namespace osu.Game.Rulesets.Objects.Drawables
protected sealed override void OnFree(HitObjectLifetimeEntry entry) protected sealed override void OnFree(HitObjectLifetimeEntry entry)
{ {
StartTimeBindable.UnbindFrom(HitObject.StartTimeBindable); StartTimeBindable.UnbindFrom(HitObject.StartTimeBindable);
if (HitObject is IHasComboInformation combo) if (HitObject is IHasComboInformation combo)
{
comboIndexBindable.UnbindFrom(combo.ComboIndexBindable); comboIndexBindable.UnbindFrom(combo.ComboIndexBindable);
comboIndexWithOffsetsBindable.UnbindFrom(combo.ComboIndexWithOffsetsBindable);
}
samplesBindable.UnbindFrom(HitObject.SamplesBindable); samplesBindable.UnbindFrom(HitObject.SamplesBindable);
// Changes in start time trigger state updates. When a new hitobject is applied, OnApply() automatically performs a state update anyway. // Changes in start time trigger state updates. When a new hitobject is applied, OnApply() automatically performs a state update anyway.

View File

@ -118,6 +118,7 @@ namespace osu.Game.Rulesets.Objects
foreach (var n in NestedHitObjects.OfType<IHasComboInformation>()) foreach (var n in NestedHitObjects.OfType<IHasComboInformation>())
{ {
n.ComboIndexBindable.BindTo(hasCombo.ComboIndexBindable); n.ComboIndexBindable.BindTo(hasCombo.ComboIndexBindable);
n.ComboIndexWithOffsetsBindable.BindTo(hasCombo.ComboIndexWithOffsetsBindable);
n.IndexInCurrentComboBindable.BindTo(hasCombo.IndexInCurrentComboBindable); n.IndexInCurrentComboBindable.BindTo(hasCombo.IndexInCurrentComboBindable);
} }
} }

View File

@ -15,17 +15,25 @@ namespace osu.Game.Rulesets.Objects.Types
Bindable<int> IndexInCurrentComboBindable { get; } Bindable<int> IndexInCurrentComboBindable { get; }
/// <summary> /// <summary>
/// The offset of this hitobject in the current combo. /// The index of this hitobject in the current combo.
/// </summary> /// </summary>
int IndexInCurrentCombo { get; set; } int IndexInCurrentCombo { get; set; }
Bindable<int> ComboIndexBindable { get; } Bindable<int> ComboIndexBindable { get; }
/// <summary> /// <summary>
/// The offset of this combo in relation to the beatmap. /// The index of this combo in relation to the beatmap.
/// </summary> /// </summary>
int ComboIndex { get; set; } int ComboIndex { get; set; }
Bindable<int> ComboIndexWithOffsetsBindable { get; }
/// <summary>
/// The index of this combo in relation to the beatmap, with all aggregate <see cref="IHasCombo.ComboOffset"/>s applied.
/// This should be used instead of <see cref="ComboIndex"/> only when retrieving combo colours from the beatmap's skin.
/// </summary>
int ComboIndexWithOffsets { get; set; }
/// <summary> /// <summary>
/// Whether the HitObject starts a new combo. /// Whether the HitObject starts a new combo.
/// </summary> /// </summary>

View File

@ -37,7 +37,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private readonly Bindable<double> startTime; private readonly Bindable<double> startTime;
private Bindable<int> indexInCurrentComboBindable; private Bindable<int> indexInCurrentComboBindable;
private Bindable<int> comboIndexBindable; private Bindable<int> comboIndexBindable;
private Bindable<int> comboIndexWithOffsetsBindable;
private Bindable<Color4> displayColourBindable; private Bindable<Color4> displayColourBindable;
private readonly ExtendableCircle circle; private readonly ExtendableCircle circle;
@ -120,7 +123,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
indexInCurrentComboBindable.BindValueChanged(_ => updateComboIndex(), true); indexInCurrentComboBindable.BindValueChanged(_ => updateComboIndex(), true);
comboIndexBindable = comboInfo.ComboIndexBindable.GetBoundCopy(); comboIndexBindable = comboInfo.ComboIndexBindable.GetBoundCopy();
comboIndexBindable.BindValueChanged(_ => updateColour(), true); comboIndexWithOffsetsBindable = comboInfo.ComboIndexWithOffsetsBindable.GetBoundCopy();
comboIndexBindable.BindValueChanged(_ => updateColour());
comboIndexWithOffsetsBindable.BindValueChanged(_ => updateColour(), true);
skin.SourceChanged += updateColour; skin.SourceChanged += updateColour;
break; break;

View File

@ -7,8 +7,11 @@ using osu.Framework.Graphics;
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats;
using osu.Game.IO; using osu.Game.IO;
using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Objects.Types;
using osuTK.Graphics;
namespace osu.Game.Skinning namespace osu.Game.Skinning
{ {
@ -59,6 +62,9 @@ namespace osu.Game.Skinning
return base.GetConfig<TLookup, TValue>(lookup); return base.GetConfig<TLookup, TValue>(lookup);
} }
protected override IBindable<Color4> GetComboColour(IHasComboColours source, int comboIndex, IHasComboInformation combo)
=> base.GetComboColour(source, combo.ComboIndexWithOffsets, combo);
public override ISample GetSample(ISampleInfo sampleInfo) public override ISample GetSample(ISampleInfo sampleInfo)
{ {
if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacy && legacy.CustomSampleBank == 0) if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacy && legacy.CustomSampleBank == 0)

View File

@ -10,7 +10,6 @@ using osu.Framework.Bindables;
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Screens.Play;
using osu.Game.Skinning; using osu.Game.Skinning;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK.Graphics; using osuTK.Graphics;
@ -60,16 +59,12 @@ namespace osu.Game.Tests.Beatmaps
protected virtual ExposedPlayer CreateTestPlayer(bool userHasCustomColours) => new ExposedPlayer(userHasCustomColours); protected virtual ExposedPlayer CreateTestPlayer(bool userHasCustomColours) => new ExposedPlayer(userHasCustomColours);
protected class ExposedPlayer : Player protected class ExposedPlayer : TestPlayer
{ {
protected readonly bool UserHasCustomColours; protected readonly bool UserHasCustomColours;
public ExposedPlayer(bool userHasCustomColours) public ExposedPlayer(bool userHasCustomColours)
: base(new PlayerConfiguration : base(false, false)
{
AllowPause = false,
ShowResults = false,
})
{ {
UserHasCustomColours = userHasCustomColours; UserHasCustomColours = userHasCustomColours;
} }
@ -106,6 +101,8 @@ namespace osu.Game.Tests.Beatmaps
{ {
new Color4(50, 100, 150, 255), new Color4(50, 100, 150, 255),
new Color4(40, 80, 120, 255), new Color4(40, 80, 120, 255),
new Color4(25, 50, 75, 255),
new Color4(10, 20, 30, 255),
}; };
public static readonly Color4 HYPER_DASH_COLOUR = Color4.DarkBlue; public static readonly Color4 HYPER_DASH_COLOUR = Color4.DarkBlue;
@ -133,6 +130,8 @@ namespace osu.Game.Tests.Beatmaps
{ {
new Color4(150, 100, 50, 255), new Color4(150, 100, 50, 255),
new Color4(20, 20, 20, 255), new Color4(20, 20, 20, 255),
new Color4(75, 50, 25, 255),
new Color4(80, 80, 80, 255),
}; };
public static readonly Color4 HYPER_DASH_COLOUR = Color4.LightBlue; public static readonly Color4 HYPER_DASH_COLOUR = Color4.LightBlue;