1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-16 02:15:17 +08:00

Merge branch 'master' into fix-spinner-rpm-user-rate-adjust

This commit is contained in:
Dean Herbert 2020-09-28 17:45:48 +09:00 committed by GitHub
commit fd58a24183
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 419 additions and 146 deletions

View File

@ -31,6 +31,9 @@ namespace osu.Game.Rulesets.Catch.Replays
public override Replay Generate() public override Replay Generate()
{ {
if (Beatmap.HitObjects.Count == 0)
return Replay;
// todo: add support for HT DT // todo: add support for HT DT
const double dash_speed = Catcher.BASE_SPEED; const double dash_speed = Catcher.BASE_SPEED;
const double movement_speed = dash_speed / 2; const double movement_speed = dash_speed / 2;

View File

@ -46,6 +46,9 @@ namespace osu.Game.Rulesets.Mania.Replays
public override Replay Generate() public override Replay Generate()
{ {
if (Beatmap.HitObjects.Count == 0)
return Replay;
var pointGroups = generateActionPoints().GroupBy(a => a.Time).OrderBy(g => g.First().Time); var pointGroups = generateActionPoints().GroupBy(a => a.Time).OrderBy(g => g.First().Time);
var actions = new List<ManiaAction>(); var actions = new List<ManiaAction>();

View File

@ -8,6 +8,7 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osuTK; using osuTK;
@ -20,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Edit
/// Hit objects are intentionally made to fade out at a constant slower rate than in gameplay. /// Hit objects are intentionally made to fade out at a constant slower rate than in gameplay.
/// This allows a mapper to gain better historical context and use recent hitobjects as reference / snap points. /// This allows a mapper to gain better historical context and use recent hitobjects as reference / snap points.
/// </summary> /// </summary>
private const double editor_hit_object_fade_out_extension = 500; private const double editor_hit_object_fade_out_extension = 700;
public DrawableOsuEditRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods) public DrawableOsuEditRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods)
: base(ruleset, beatmap, mods) : base(ruleset, beatmap, mods)
@ -32,11 +33,30 @@ namespace osu.Game.Rulesets.Osu.Edit
private void updateState(DrawableHitObject hitObject, ArmedState state) private void updateState(DrawableHitObject hitObject, ArmedState state)
{ {
switch (state) if (state == ArmedState.Idle)
return;
// adjust the visuals of certain object types to make them stay on screen for longer than usual.
switch (hitObject)
{ {
case ArmedState.Miss: default:
// there are quite a few drawable hit types we don't want to extent (spinners, ticks etc.)
return;
case DrawableSlider _:
// no specifics to sliders but let them fade slower below.
break;
case DrawableHitCircle circle: // also handles slider heads
circle.ApproachCircle
.FadeOutFromOne(editor_hit_object_fade_out_extension)
.Expire();
break;
}
// Get the existing fade out transform // Get the existing fade out transform
var existing = hitObject.Transforms.LastOrDefault(t => t.TargetMember == nameof(Alpha)); var existing = hitObject.Transforms.LastOrDefault(t => t.TargetMember == nameof(Alpha));
if (existing == null) if (existing == null)
return; return;
@ -44,8 +64,6 @@ namespace osu.Game.Rulesets.Osu.Edit
using (hitObject.BeginAbsoluteSequence(existing.StartTime)) using (hitObject.BeginAbsoluteSequence(existing.StartTime))
hitObject.FadeOut(editor_hit_object_fade_out_extension).Expire(); hitObject.FadeOut(editor_hit_object_fade_out_extension).Expire();
break;
}
} }
protected override Playfield CreatePlayfield() => new OsuPlayfieldNoCursor(); protected override Playfield CreatePlayfield() => new OsuPlayfieldNoCursor();

View File

@ -72,6 +72,9 @@ namespace osu.Game.Rulesets.Osu.Replays
public override Replay Generate() public override Replay Generate()
{ {
if (Beatmap.HitObjects.Count == 0)
return Replay;
buttonIndex = 0; buttonIndex = 0;
AddFrameToReplay(new OsuReplayFrame(-100000, new Vector2(256, 500))); AddFrameToReplay(new OsuReplayFrame(-100000, new Vector2(256, 500)));

View File

@ -0,0 +1,55 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.UI;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Taiko.Tests
{
public abstract class DrawableTaikoRulesetTestScene : OsuTestScene
{
protected DrawableTaikoRuleset DrawableRuleset { get; private set; }
protected Container PlayfieldContainer { get; private set; }
[BackgroundDependencyLoader]
private void load()
{
var controlPointInfo = new ControlPointInfo();
controlPointInfo.Add(0, new TimingControlPoint());
WorkingBeatmap beatmap = CreateWorkingBeatmap(new Beatmap
{
HitObjects = new List<HitObject> { new Hit { Type = HitType.Centre } },
BeatmapInfo = new BeatmapInfo
{
BaseDifficulty = new BeatmapDifficulty(),
Metadata = new BeatmapMetadata
{
Artist = @"Unknown",
Title = @"Sample Beatmap",
AuthorString = @"peppy",
},
Ruleset = new TaikoRuleset().RulesetInfo
},
ControlPointInfo = controlPointInfo
});
Add(PlayfieldContainer = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
Height = 768,
Children = new[] { DrawableRuleset = new DrawableTaikoRuleset(new TaikoRuleset(), beatmap.GetPlayableBeatmap(new TaikoRuleset().RulesetInfo)) }
});
}
}
}

View File

@ -2,26 +2,36 @@
// 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 osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.Taiko.Objects.Drawables;
namespace osu.Game.Rulesets.Taiko.Tests namespace osu.Game.Rulesets.Taiko.Tests
{ {
internal class DrawableTestHit : DrawableTaikoHitObject public class DrawableTestHit : DrawableHit
{ {
private readonly HitResult type; public readonly HitResult Type;
public DrawableTestHit(Hit hit, HitResult type = HitResult.Great) public DrawableTestHit(Hit hit, HitResult type = HitResult.Great)
: base(hit) : base(hit)
{ {
this.type = type; Type = type;
HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
}
protected override void UpdateInitialTransforms()
{
// base implementation in DrawableHitObject forces alpha to 1.
// suppress locally to allow hiding the visuals wherever necessary.
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
Result.Type = type; Result.Type = Type;
} }
public override bool OnPressed(TaikoAction action) => false; public override bool OnPressed(TaikoAction action) => false;

View File

@ -2,17 +2,14 @@
// 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.Linq; using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.Taiko.Objects.Drawables;
namespace osu.Game.Rulesets.Taiko.Tests namespace osu.Game.Rulesets.Taiko.Tests
{ {
public class DrawableTestStrongHit : DrawableHit public class DrawableTestStrongHit : DrawableTestHit
{ {
private readonly HitResult type;
private readonly bool hitBoth; private readonly bool hitBoth;
public DrawableTestStrongHit(double startTime, HitResult type = HitResult.Great, bool hitBoth = true) public DrawableTestStrongHit(double startTime, HitResult type = HitResult.Great, bool hitBoth = true)
@ -20,12 +17,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
{ {
IsStrong = true, IsStrong = true,
StartTime = startTime, StartTime = startTime,
}) }, type)
{ {
// in order to create nested strong hit
HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
this.type = type;
this.hitBoth = hitBoth; this.hitBoth = hitBoth;
} }
@ -33,10 +26,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
{ {
base.LoadAsyncComplete(); base.LoadAsyncComplete();
Result.Type = type;
var nestedStrongHit = (DrawableStrongNestedHit)NestedHitObjects.Single(); var nestedStrongHit = (DrawableStrongNestedHit)NestedHitObjects.Single();
nestedStrongHit.Result.Type = hitBoth ? type : HitResult.Miss; nestedStrongHit.Result.Type = hitBoth ? Type : HitResult.Miss;
} }
public override bool OnPressed(TaikoAction action) => false; public override bool OnPressed(TaikoAction action) => false;

View File

@ -212,7 +212,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
foreach (var playfield in playfields) foreach (var playfield in playfields)
{ {
var hit = new DrawableTestHit(new Hit(), judgementResult.Type); var hit = new DrawableTestHit(new Hit(), judgementResult.Type);
Add(hit); playfield.Add(hit);
playfield.OnNewResult(hit, judgementResult); playfield.OnNewResult(hit, judgementResult);
} }

View File

@ -6,7 +6,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Rulesets.Taiko.UI; using osu.Game.Rulesets.Taiko.UI;
namespace osu.Game.Rulesets.Taiko.Tests.Skinning namespace osu.Game.Rulesets.Taiko.Tests.Skinning
@ -29,15 +28,17 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
AddStep("Good", () => SetContents(() => getContentFor(createStrongHit(HitResult.Good, hitBoth)))); AddStep("Good", () => SetContents(() => getContentFor(createStrongHit(HitResult.Good, hitBoth))));
} }
private Drawable getContentFor(DrawableTaikoHitObject hit) private Drawable getContentFor(DrawableTestHit hit)
{ {
return new Container return new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Children = new Drawable[] Children = new Drawable[]
{ {
hit, // the hit needs to be added to hierarchy in order for nested objects to be created correctly.
new HitExplosion(hit) // setting zero alpha is supposed to prevent the test from looking broken.
hit.With(h => h.Alpha = 0),
new HitExplosion(hit, hit.Type)
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -46,9 +47,8 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
}; };
} }
private DrawableTaikoHitObject createHit(HitResult type) => new DrawableTestHit(new Hit { StartTime = Time.Current }, type); private DrawableTestHit createHit(HitResult type) => new DrawableTestHit(new Hit { StartTime = Time.Current }, type);
private DrawableTaikoHitObject createStrongHit(HitResult type, bool hitBoth) private DrawableTestHit createStrongHit(HitResult type, bool hitBoth) => new DrawableTestStrongHit(Time.Current, type, hitBoth);
=> new DrawableTestStrongHit(Time.Current, type, hitBoth);
} }
} }

View File

@ -0,0 +1,48 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Judgements;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Rulesets.Taiko.UI;
namespace osu.Game.Rulesets.Taiko.Tests
{
[TestFixture]
public class TestSceneFlyingHits : DrawableTaikoRulesetTestScene
{
[TestCase(HitType.Centre)]
[TestCase(HitType.Rim)]
public void TestFlyingHits(HitType hitType)
{
DrawableFlyingHit flyingHit = null;
AddStep("add flying hit", () =>
{
addFlyingHit(hitType);
// flying hits all land in one common scrolling container (and stay there for rewind purposes),
// so we need to manually get the latest one.
flyingHit = this.ChildrenOfType<DrawableFlyingHit>()
.OrderByDescending(h => h.HitObject.StartTime)
.FirstOrDefault();
});
AddAssert("hit type is correct", () => flyingHit.HitObject.Type == hitType);
}
private void addFlyingHit(HitType hitType)
{
var tick = new DrumRollTick { HitWindows = HitWindows.Empty, StartTime = DrawableRuleset.Playfield.Time.Current };
DrawableDrumRollTick h;
DrawableRuleset.Playfield.Add(h = new DrawableDrumRollTick(tick) { JudgementType = hitType });
((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h, new JudgementResult(tick, new TaikoDrumRollTickJudgement()) { Type = HitResult.Perfect });
}
}
}

View File

@ -2,11 +2,9 @@
// 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 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.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
@ -18,13 +16,12 @@ using osu.Game.Rulesets.Taiko.Judgements;
using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Rulesets.Taiko.UI; using osu.Game.Rulesets.Taiko.UI;
using osu.Game.Tests.Visual;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Taiko.Tests namespace osu.Game.Rulesets.Taiko.Tests
{ {
[TestFixture] [TestFixture]
public class TestSceneHits : OsuTestScene public class TestSceneHits : DrawableTaikoRulesetTestScene
{ {
private const double default_duration = 3000; private const double default_duration = 3000;
private const float scroll_time = 1000; private const float scroll_time = 1000;
@ -32,8 +29,6 @@ namespace osu.Game.Rulesets.Taiko.Tests
protected override double TimePerAction => default_duration * 2; protected override double TimePerAction => default_duration * 2;
private readonly Random rng = new Random(1337); private readonly Random rng = new Random(1337);
private DrawableTaikoRuleset drawableRuleset;
private Container playfieldContainer;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
@ -64,35 +59,6 @@ namespace osu.Game.Rulesets.Taiko.Tests
AddStep("Height test 4", () => changePlayfieldSize(4)); AddStep("Height test 4", () => changePlayfieldSize(4));
AddStep("Height test 5", () => changePlayfieldSize(5)); AddStep("Height test 5", () => changePlayfieldSize(5));
AddStep("Reset height", () => changePlayfieldSize(6)); AddStep("Reset height", () => changePlayfieldSize(6));
var controlPointInfo = new ControlPointInfo();
controlPointInfo.Add(0, new TimingControlPoint());
WorkingBeatmap beatmap = CreateWorkingBeatmap(new Beatmap
{
HitObjects = new List<HitObject> { new Hit { Type = HitType.Centre } },
BeatmapInfo = new BeatmapInfo
{
BaseDifficulty = new BeatmapDifficulty(),
Metadata = new BeatmapMetadata
{
Artist = @"Unknown",
Title = @"Sample Beatmap",
AuthorString = @"peppy",
},
Ruleset = new TaikoRuleset().RulesetInfo
},
ControlPointInfo = controlPointInfo
});
Add(playfieldContainer = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
Height = 768,
Children = new[] { drawableRuleset = new DrawableTaikoRuleset(new TaikoRuleset(), beatmap.GetPlayableBeatmap(new TaikoRuleset().RulesetInfo)) }
});
} }
private void changePlayfieldSize(int step) private void changePlayfieldSize(int step)
@ -128,11 +94,11 @@ namespace osu.Game.Rulesets.Taiko.Tests
switch (step) switch (step)
{ {
default: default:
playfieldContainer.Delay(delay).ResizeTo(new Vector2(1, rng.Next(25, 400)), 500); PlayfieldContainer.Delay(delay).ResizeTo(new Vector2(1, rng.Next(25, 400)), 500);
break; break;
case 6: case 6:
playfieldContainer.Delay(delay).ResizeTo(new Vector2(1, TaikoPlayfield.DEFAULT_HEIGHT), 500); PlayfieldContainer.Delay(delay).ResizeTo(new Vector2(1, TaikoPlayfield.DEFAULT_HEIGHT), 500);
break; break;
} }
} }
@ -149,9 +115,9 @@ namespace osu.Game.Rulesets.Taiko.Tests
var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Good ? -0.1f : -0.05f, hitResult == HitResult.Good ? 0.1f : 0.05f) }; var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Good ? -0.1f : -0.05f, hitResult == HitResult.Good ? 0.1f : 0.05f) };
Add(h); DrawableRuleset.Playfield.Add(h);
((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = hitResult }); ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = hitResult });
} }
private void addStrongHitJudgement(bool kiai) private void addStrongHitJudgement(bool kiai)
@ -166,37 +132,37 @@ namespace osu.Game.Rulesets.Taiko.Tests
var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Good ? -0.1f : -0.05f, hitResult == HitResult.Good ? 0.1f : 0.05f) }; var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Good ? -0.1f : -0.05f, hitResult == HitResult.Good ? 0.1f : 0.05f) };
Add(h); DrawableRuleset.Playfield.Add(h);
((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = hitResult }); ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = hitResult });
((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(new TestStrongNestedHit(h), new JudgementResult(new HitObject(), new TaikoStrongJudgement()) { Type = HitResult.Great }); ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(new TestStrongNestedHit(h), new JudgementResult(new HitObject(), new TaikoStrongJudgement()) { Type = HitResult.Great });
} }
private void addMissJudgement() private void addMissJudgement()
{ {
DrawableTestHit h; DrawableTestHit h;
Add(h = new DrawableTestHit(new Hit(), HitResult.Miss)); DrawableRuleset.Playfield.Add(h = new DrawableTestHit(new Hit(), HitResult.Miss));
((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = HitResult.Miss }); ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = HitResult.Miss });
} }
private void addBarLine(bool major, double delay = scroll_time) private void addBarLine(bool major, double delay = scroll_time)
{ {
BarLine bl = new BarLine { StartTime = drawableRuleset.Playfield.Time.Current + delay }; BarLine bl = new BarLine { StartTime = DrawableRuleset.Playfield.Time.Current + delay };
drawableRuleset.Playfield.Add(major ? new DrawableBarLineMajor(bl) : new DrawableBarLine(bl)); DrawableRuleset.Playfield.Add(major ? new DrawableBarLineMajor(bl) : new DrawableBarLine(bl));
} }
private void addSwell(double duration = default_duration) private void addSwell(double duration = default_duration)
{ {
var swell = new Swell var swell = new Swell
{ {
StartTime = drawableRuleset.Playfield.Time.Current + scroll_time, StartTime = DrawableRuleset.Playfield.Time.Current + scroll_time,
Duration = duration, Duration = duration,
}; };
swell.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); swell.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
drawableRuleset.Playfield.Add(new DrawableSwell(swell)); DrawableRuleset.Playfield.Add(new DrawableSwell(swell));
} }
private void addDrumRoll(bool strong, double duration = default_duration, bool kiai = false) private void addDrumRoll(bool strong, double duration = default_duration, bool kiai = false)
@ -206,7 +172,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
var d = new DrumRoll var d = new DrumRoll
{ {
StartTime = drawableRuleset.Playfield.Time.Current + scroll_time, StartTime = DrawableRuleset.Playfield.Time.Current + scroll_time,
IsStrong = strong, IsStrong = strong,
Duration = duration, Duration = duration,
TickRate = 8, TickRate = 8,
@ -217,33 +183,33 @@ namespace osu.Game.Rulesets.Taiko.Tests
d.ApplyDefaults(cpi, new BeatmapDifficulty()); d.ApplyDefaults(cpi, new BeatmapDifficulty());
drawableRuleset.Playfield.Add(new DrawableDrumRoll(d)); DrawableRuleset.Playfield.Add(new DrawableDrumRoll(d));
} }
private void addCentreHit(bool strong) private void addCentreHit(bool strong)
{ {
Hit h = new Hit Hit h = new Hit
{ {
StartTime = drawableRuleset.Playfield.Time.Current + scroll_time, StartTime = DrawableRuleset.Playfield.Time.Current + scroll_time,
IsStrong = strong IsStrong = strong
}; };
h.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); h.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
drawableRuleset.Playfield.Add(new DrawableHit(h)); DrawableRuleset.Playfield.Add(new DrawableHit(h));
} }
private void addRimHit(bool strong) private void addRimHit(bool strong)
{ {
Hit h = new Hit Hit h = new Hit
{ {
StartTime = drawableRuleset.Playfield.Time.Current + scroll_time, StartTime = DrawableRuleset.Playfield.Time.Current + scroll_time,
IsStrong = strong IsStrong = strong
}; };
h.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); h.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
drawableRuleset.Playfield.Add(new DrawableHit(h)); DrawableRuleset.Playfield.Add(new DrawableHit(h));
} }
private class TestStrongNestedHit : DrawableStrongNestedHit private class TestStrongNestedHit : DrawableStrongNestedHit

View File

@ -57,7 +57,13 @@ namespace osu.Game.Rulesets.Taiko.Edit
ChangeHandler.BeginChange(); ChangeHandler.BeginChange();
foreach (var h in hits) foreach (var h in hits)
{
if (h.IsStrong != state)
{
h.IsStrong = state; h.IsStrong = state;
EditorBeatmap.UpdateHitObject(h);
}
}
ChangeHandler.EndChange(); ChangeHandler.EndChange();
} }

View File

@ -27,5 +27,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
base.LoadComplete(); base.LoadComplete();
ApplyResult(r => r.Type = r.Judgement.MaxResult); ApplyResult(r => r.Type = r.Judgement.MaxResult);
} }
protected override void LoadSamples()
{
// block base call - flying hits are not supposed to play samples
// the base call could overwrite the type of this hit
}
} }
} }

View File

@ -30,6 +30,9 @@ namespace osu.Game.Rulesets.Taiko.Replays
public override Replay Generate() public override Replay Generate()
{ {
if (Beatmap.HitObjects.Count == 0)
return Replay;
bool hitButton = true; bool hitButton = true;
Frames.Add(new TaikoReplayFrame(-100000)); Frames.Add(new TaikoReplayFrame(-100000));

View File

@ -15,8 +15,14 @@ namespace osu.Game.Rulesets.Taiko.UI
{ {
internal class DefaultHitExplosion : CircularContainer internal class DefaultHitExplosion : CircularContainer
{ {
[Resolved] private readonly DrawableHitObject judgedObject;
private DrawableHitObject judgedObject { get; set; } private readonly HitResult result;
public DefaultHitExplosion(DrawableHitObject judgedObject, HitResult result)
{
this.judgedObject = judgedObject;
this.result = result;
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours)
@ -31,7 +37,7 @@ namespace osu.Game.Rulesets.Taiko.UI
Alpha = 0.15f; Alpha = 0.15f;
Masking = true; Masking = true;
if (judgedObject.Result.Type == HitResult.Miss) if (result == HitResult.Miss)
return; return;
bool isRim = (judgedObject.HitObject as Hit)?.Type == HitType.Rim; bool isRim = (judgedObject.HitObject as Hit)?.Type == HitType.Rim;

View File

@ -25,15 +25,18 @@ namespace osu.Game.Rulesets.Taiko.UI
[Cached(typeof(DrawableHitObject))] [Cached(typeof(DrawableHitObject))]
public readonly DrawableHitObject JudgedObject; public readonly DrawableHitObject JudgedObject;
private readonly HitResult result;
private SkinnableDrawable skinnable; private SkinnableDrawable skinnable;
public override double LifetimeStart => skinnable.Drawable.LifetimeStart; public override double LifetimeStart => skinnable.Drawable.LifetimeStart;
public override double LifetimeEnd => skinnable.Drawable.LifetimeEnd; public override double LifetimeEnd => skinnable.Drawable.LifetimeEnd;
public HitExplosion(DrawableHitObject judgedObject) public HitExplosion(DrawableHitObject judgedObject, HitResult result)
{ {
JudgedObject = judgedObject; JudgedObject = judgedObject;
this.result = result;
Anchor = Anchor.Centre; Anchor = Anchor.Centre;
Origin = Anchor.Centre; Origin = Anchor.Centre;
@ -47,14 +50,12 @@ namespace osu.Game.Rulesets.Taiko.UI
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
Child = skinnable = new SkinnableDrawable(new TaikoSkinComponent(getComponentName(JudgedObject)), _ => new DefaultHitExplosion()); Child = skinnable = new SkinnableDrawable(new TaikoSkinComponent(getComponentName(JudgedObject)), _ => new DefaultHitExplosion(JudgedObject, result));
} }
private TaikoSkinComponents getComponentName(DrawableHitObject judgedObject) private TaikoSkinComponents getComponentName(DrawableHitObject judgedObject)
{ {
var resultType = judgedObject.Result?.Type ?? HitResult.Great; switch (result)
switch (resultType)
{ {
case HitResult.Miss: case HitResult.Miss:
return TaikoSkinComponents.TaikoExplosionMiss; return TaikoSkinComponents.TaikoExplosionMiss;

View File

@ -9,6 +9,7 @@ using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.Taiko.Objects.Drawables;
@ -206,8 +207,7 @@ namespace osu.Game.Rulesets.Taiko.UI
}); });
var type = (judgedObject.HitObject as Hit)?.Type ?? HitType.Centre; var type = (judgedObject.HitObject as Hit)?.Type ?? HitType.Centre;
addExplosion(judgedObject, result.Type, type);
addExplosion(judgedObject, type);
break; break;
} }
} }
@ -219,9 +219,9 @@ namespace osu.Game.Rulesets.Taiko.UI
/// As legacy skins have different explosions for singular and double strong hits, /// As legacy skins have different explosions for singular and double strong hits,
/// explosion addition is scheduled to ensure that both hits are processed if they occur on the same frame. /// explosion addition is scheduled to ensure that both hits are processed if they occur on the same frame.
/// </remarks> /// </remarks>
private void addExplosion(DrawableHitObject drawableObject, HitType type) => Schedule(() => private void addExplosion(DrawableHitObject drawableObject, HitResult result, HitType type) => Schedule(() =>
{ {
hitExplosionContainer.Add(new HitExplosion(drawableObject)); hitExplosionContainer.Add(new HitExplosion(drawableObject, result));
if (drawableObject.HitObject.Kiai) if (drawableObject.HitObject.Kiai)
kiaiExplosionContainer.Add(new KiaiHitExplosion(drawableObject, type)); kiaiExplosionContainer.Add(new KiaiHitExplosion(drawableObject, type));
}); });

View File

@ -2,7 +2,9 @@
// 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 NUnit.Framework; using NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
namespace osu.Game.Tests.Editing namespace osu.Game.Tests.Editing
@ -13,11 +15,12 @@ namespace osu.Game.Tests.Editing
[Test] [Test]
public void TestSaveRestoreState() public void TestSaveRestoreState()
{ {
var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap())); var (handler, beatmap) = createChangeHandler();
Assert.That(handler.CanUndo.Value, Is.False); Assert.That(handler.CanUndo.Value, Is.False);
Assert.That(handler.CanRedo.Value, Is.False); Assert.That(handler.CanRedo.Value, Is.False);
addArbitraryChange(beatmap);
handler.SaveState(); handler.SaveState();
Assert.That(handler.CanUndo.Value, Is.True); Assert.That(handler.CanUndo.Value, Is.True);
@ -29,15 +32,48 @@ namespace osu.Game.Tests.Editing
Assert.That(handler.CanRedo.Value, Is.True); Assert.That(handler.CanRedo.Value, Is.True);
} }
[Test]
public void TestSaveSameStateDoesNotSave()
{
var (handler, beatmap) = createChangeHandler();
Assert.That(handler.CanUndo.Value, Is.False);
Assert.That(handler.CanRedo.Value, Is.False);
addArbitraryChange(beatmap);
handler.SaveState();
Assert.That(handler.CanUndo.Value, Is.True);
Assert.That(handler.CanRedo.Value, Is.False);
string hash = handler.CurrentStateHash;
// save a save without making any changes
handler.SaveState();
Assert.That(hash, Is.EqualTo(handler.CurrentStateHash));
handler.RestoreState(-1);
Assert.That(hash, Is.Not.EqualTo(handler.CurrentStateHash));
// we should only be able to restore once even though we saved twice.
Assert.That(handler.CanUndo.Value, Is.False);
Assert.That(handler.CanRedo.Value, Is.True);
}
[Test] [Test]
public void TestMaxStatesSaved() public void TestMaxStatesSaved()
{ {
var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap())); var (handler, beatmap) = createChangeHandler();
Assert.That(handler.CanUndo.Value, Is.False); Assert.That(handler.CanUndo.Value, Is.False);
for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES; i++) for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES; i++)
{
addArbitraryChange(beatmap);
handler.SaveState(); handler.SaveState();
}
Assert.That(handler.CanUndo.Value, Is.True); Assert.That(handler.CanUndo.Value, Is.True);
@ -53,12 +89,15 @@ namespace osu.Game.Tests.Editing
[Test] [Test]
public void TestMaxStatesExceeded() public void TestMaxStatesExceeded()
{ {
var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap())); var (handler, beatmap) = createChangeHandler();
Assert.That(handler.CanUndo.Value, Is.False); Assert.That(handler.CanUndo.Value, Is.False);
for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES * 2; i++) for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES * 2; i++)
{
addArbitraryChange(beatmap);
handler.SaveState(); handler.SaveState();
}
Assert.That(handler.CanUndo.Value, Is.True); Assert.That(handler.CanUndo.Value, Is.True);
@ -70,5 +109,17 @@ namespace osu.Game.Tests.Editing
Assert.That(handler.CanUndo.Value, Is.False); Assert.That(handler.CanUndo.Value, Is.False);
} }
private (EditorChangeHandler, EditorBeatmap) createChangeHandler()
{
var beatmap = new EditorBeatmap(new Beatmap());
return (new EditorChangeHandler(beatmap), beatmap);
}
private void addArbitraryChange(EditorBeatmap beatmap)
{
beatmap.Add(new HitCircle { StartTime = RNG.Next(0, 100000) });
}
} }
} }

View File

@ -1,15 +1,18 @@
// 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.Threading;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Timing; using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
@ -175,6 +178,24 @@ namespace osu.Game.Tests.Gameplay
assertHealthNotEqualTo(0); assertHealthNotEqualTo(0);
} }
[Test]
public void TestSingleLongObjectDoesNotDrain()
{
var beatmap = new Beatmap
{
HitObjects = { new JudgeableLongHitObject() }
};
beatmap.HitObjects[0].ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
createProcessor(beatmap);
setTime(0);
assertHealthEqualTo(1);
setTime(5000);
assertHealthEqualTo(1);
}
private Beatmap createBeatmap(double startTime, double endTime, params BreakPeriod[] breaks) private Beatmap createBeatmap(double startTime, double endTime, params BreakPeriod[] breaks)
{ {
var beatmap = new Beatmap var beatmap = new Beatmap
@ -235,5 +256,23 @@ namespace osu.Game.Tests.Gameplay
} }
} }
} }
private class JudgeableLongHitObject : JudgeableHitObject, IHasDuration
{
public double EndTime => StartTime + Duration;
public double Duration { get; set; } = 5000;
public JudgeableLongHitObject()
: base(false)
{
}
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{
base.CreateNestedHitObjects(cancellationToken);
AddNested(new JudgeableHitObject());
}
}
} }
} }

View File

@ -12,6 +12,14 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
protected new OverlayTestPlayer Player => base.Player as OverlayTestPlayer; protected new OverlayTestPlayer Player => base.Player as OverlayTestPlayer;
public override void SetUpSteps()
{
base.SetUpSteps();
AddUntilStep("gameplay has started",
() => Player.GameplayClockContainer.GameplayClock.CurrentTime > Player.DrawableRuleset.GameplayStartTime);
}
[Test] [Test]
public void TestGameplayOverlayActivation() public void TestGameplayOverlayActivation()
{ {
@ -21,7 +29,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestGameplayOverlayActivationPaused() public void TestGameplayOverlayActivationPaused()
{ {
AddUntilStep("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled); AddAssert("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled);
AddStep("pause gameplay", () => Player.Pause()); AddStep("pause gameplay", () => Player.Pause());
AddUntilStep("activation mode is user triggered", () => Player.OverlayActivationMode == OverlayActivation.UserTriggered); AddUntilStep("activation mode is user triggered", () => Player.OverlayActivationMode == OverlayActivation.UserTriggered);
} }

View File

@ -112,6 +112,9 @@ namespace osu.Game.Graphics.Containers
CornerRadius = 5; CornerRadius = 5;
// needs to be set initially for the ResizeTo to respect minimum size
Size = new Vector2(SCROLL_BAR_HEIGHT);
const float margin = 3; const float margin = 3;
Margin = new MarginPadding Margin = new MarginPadding

View File

@ -45,15 +45,21 @@ namespace osu.Game.Rulesets.Edit
base.LoadComplete(); base.LoadComplete();
beatmap.HitObjectAdded += addHitObject; beatmap.HitObjectAdded += addHitObject;
beatmap.HitObjectUpdated += updateReplay;
beatmap.HitObjectRemoved += removeHitObject; beatmap.HitObjectRemoved += removeHitObject;
} }
private void updateReplay(HitObject obj = null) =>
drawableRuleset.RegenerateAutoplay();
private void addHitObject(HitObject hitObject) private void addHitObject(HitObject hitObject)
{ {
var drawableObject = drawableRuleset.CreateDrawableRepresentation((TObject)hitObject); var drawableObject = drawableRuleset.CreateDrawableRepresentation((TObject)hitObject);
drawableRuleset.Playfield.Add(drawableObject); drawableRuleset.Playfield.Add(drawableObject);
drawableRuleset.Playfield.PostProcess(); drawableRuleset.Playfield.PostProcess();
updateReplay();
} }
private void removeHitObject(HitObject hitObject) private void removeHitObject(HitObject hitObject)
@ -62,6 +68,8 @@ namespace osu.Game.Rulesets.Edit
drawableRuleset.Playfield.Remove(drawableObject); drawableRuleset.Playfield.Remove(drawableObject);
drawableRuleset.Playfield.PostProcess(); drawableRuleset.Playfield.PostProcess();
drawableRuleset.RegenerateAutoplay();
} }
public override bool PropagatePositionalInputSubTree => false; public override bool PropagatePositionalInputSubTree => false;

View File

@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Edit
try try
{ {
drawableRulesetWrapper = new DrawableEditRulesetWrapper<TObject>(CreateDrawableRuleset(Ruleset, EditorBeatmap.PlayableBeatmap)) drawableRulesetWrapper = new DrawableEditRulesetWrapper<TObject>(CreateDrawableRuleset(Ruleset, EditorBeatmap.PlayableBeatmap, new[] { Ruleset.GetAutoplayMod() }))
{ {
Clock = EditorClock, Clock = EditorClock,
ProcessCustomClock = false ProcessCustomClock = false

View File

@ -133,7 +133,7 @@ namespace osu.Game.Rulesets.Scoring
private double computeDrainRate() private double computeDrainRate()
{ {
if (healthIncreases.Count == 0) if (healthIncreases.Count <= 1)
return 0; return 0;
int adjustment = 1; int adjustment = 1;

View File

@ -151,8 +151,11 @@ namespace osu.Game.Rulesets.UI
public virtual PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new PlayfieldAdjustmentContainer(); public virtual PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new PlayfieldAdjustmentContainer();
[Resolved]
private OsuConfigManager config { get; set; }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuConfigManager config, CancellationToken? cancellationToken) private void load(CancellationToken? cancellationToken)
{ {
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
@ -178,11 +181,18 @@ namespace osu.Game.Rulesets.UI
.WithChild(ResumeOverlay))); .WithChild(ResumeOverlay)));
} }
applyRulesetMods(Mods, config); RegenerateAutoplay();
loadObjects(cancellationToken); loadObjects(cancellationToken);
} }
public void RegenerateAutoplay()
{
// for now this is applying mods which aren't just autoplay.
// we'll need to reconsider this flow in the future.
applyRulesetMods(Mods, config);
}
/// <summary> /// <summary>
/// Creates and adds drawable representations of hit objects to the play field. /// Creates and adds drawable representations of hit objects to the play field.
/// </summary> /// </summary>

View File

@ -126,9 +126,8 @@ namespace osu.Game.Rulesets.UI
try try
{ {
if (!FrameStablePlayback) if (FrameStablePlayback)
return; {
if (firstConsumption) if (firstConsumption)
{ {
// On the first update, frame-stability seeking would result in unexpected/unwanted behaviour. // On the first update, frame-stability seeking would result in unexpected/unwanted behaviour.
@ -148,18 +147,43 @@ namespace osu.Game.Rulesets.UI
? Math.Min(newProposedTime, manualClock.CurrentTime + sixty_frame_time) ? Math.Min(newProposedTime, manualClock.CurrentTime + sixty_frame_time)
: Math.Max(newProposedTime, manualClock.CurrentTime - sixty_frame_time); : Math.Max(newProposedTime, manualClock.CurrentTime - sixty_frame_time);
} }
}
if (isAttached) if (isAttached)
{ {
double? newTime = ReplayInputHandler.SetFrameFromTime(newProposedTime); double? newTime;
if (FrameStablePlayback)
{
// when stability is turned on, we shouldn't execute for time values the replay is unable to satisfy.
if ((newTime = ReplayInputHandler.SetFrameFromTime(newProposedTime)) == null)
{
// setting invalid state here ensures that gameplay will not continue (ie. our child
// hierarchy won't be updated).
validState = false;
// potentially loop to catch-up playback.
requireMoreUpdateLoops = true;
return;
}
}
else
{
// when stability is disabled, we don't really care about accuracy.
// looping over the replay will allow it to catch up and feed out the required values
// for the current time.
while ((newTime = ReplayInputHandler.SetFrameFromTime(newProposedTime)) != newProposedTime)
{
if (newTime == null) if (newTime == null)
{ {
// we shouldn't execute for this time value. probably waiting on more replay data. // special case for when the replay actually can't arrive at the required time.
// protects from potential endless loop.
validState = false; validState = false;
requireMoreUpdateLoops = true;
return; return;
} }
}
}
newProposedTime = newTime.Value; newProposedTime = newTime.Value;
} }

View File

@ -201,6 +201,10 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (isDraggingBlueprint) if (isDraggingBlueprint)
{ {
// handle positional change etc.
foreach (var obj in selectedHitObjects)
Beatmap.UpdateHitObject(obj);
changeHandler?.EndChange(); changeHandler?.EndChange();
isDraggingBlueprint = false; isDraggingBlueprint = false;
} }

View File

@ -212,6 +212,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (blueprint != null) if (blueprint != null)
{ {
// doing this post-creations as adding the default hit sample should be the case regardless of the ruleset.
blueprint.HitObject.Samples.Add(new HitSampleInfo { Name = HitSampleInfo.HIT_NORMAL });
placementBlueprintContainer.Child = currentPlacement = blueprint; placementBlueprintContainer.Child = currentPlacement = blueprint;
// Fixes a 1-frame position discrepancy due to the first mouse move event happening in the next frame // Fixes a 1-frame position discrepancy due to the first mouse move event happening in the next frame

View File

@ -288,8 +288,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
{ {
var comboInfo = h as IHasComboInformation; var comboInfo = h as IHasComboInformation;
if (comboInfo == null) if (comboInfo == null || comboInfo.NewCombo == state) continue;
continue;
comboInfo.NewCombo = state; comboInfo.NewCombo = state;
EditorBeatmap?.UpdateHitObject(h); EditorBeatmap?.UpdateHitObject(h);

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using System.Text; using System.Text;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions; using osu.Framework.Extensions;
@ -89,24 +90,28 @@ namespace osu.Game.Screens.Edit
if (isRestoring) if (isRestoring)
return; return;
using (var stream = new MemoryStream())
{
using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
new LegacyBeatmapEncoder(editorBeatmap, editorBeatmap.BeatmapSkin).Encode(sw);
var newState = stream.ToArray();
// if the previous state is binary equal we don't need to push a new one, unless this is the initial state.
if (savedStates.Count > 0 && newState.SequenceEqual(savedStates.Last())) return;
if (currentState < savedStates.Count - 1) if (currentState < savedStates.Count - 1)
savedStates.RemoveRange(currentState + 1, savedStates.Count - currentState - 1); savedStates.RemoveRange(currentState + 1, savedStates.Count - currentState - 1);
if (savedStates.Count > MAX_SAVED_STATES) if (savedStates.Count > MAX_SAVED_STATES)
savedStates.RemoveAt(0); savedStates.RemoveAt(0);
using (var stream = new MemoryStream()) savedStates.Add(newState);
{
using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
new LegacyBeatmapEncoder(editorBeatmap, editorBeatmap.BeatmapSkin).Encode(sw);
savedStates.Add(stream.ToArray());
}
currentState = savedStates.Count - 1; currentState = savedStates.Count - 1;
updateBindables(); updateBindables();
} }
}
/// <summary> /// <summary>
/// Restores an older or newer state. /// Restores an older or newer state.