1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-15 02:33:02 +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()
{
if (Beatmap.HitObjects.Count == 0)
return Replay;
// todo: add support for HT DT
const double dash_speed = Catcher.BASE_SPEED;
const double movement_speed = dash_speed / 2;

View File

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

View File

@ -8,6 +8,7 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI;
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.
/// This allows a mapper to gain better historical context and use recent hitobjects as reference / snap points.
/// </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)
: base(ruleset, beatmap, mods)
@ -32,20 +33,37 @@ namespace osu.Game.Rulesets.Osu.Edit
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:
// Get the existing fade out transform
var existing = hitObject.Transforms.LastOrDefault(t => t.TargetMember == nameof(Alpha));
if (existing == null)
return;
default:
// there are quite a few drawable hit types we don't want to extent (spinners, ticks etc.)
return;
hitObject.RemoveTransform(existing);
case DrawableSlider _:
// no specifics to sliders but let them fade slower below.
break;
using (hitObject.BeginAbsoluteSequence(existing.StartTime))
hitObject.FadeOut(editor_hit_object_fade_out_extension).Expire();
case DrawableHitCircle circle: // also handles slider heads
circle.ApproachCircle
.FadeOutFromOne(editor_hit_object_fade_out_extension)
.Expire();
break;
}
// Get the existing fade out transform
var existing = hitObject.Transforms.LastOrDefault(t => t.TargetMember == nameof(Alpha));
if (existing == null)
return;
hitObject.RemoveTransform(existing);
using (hitObject.BeginAbsoluteSequence(existing.StartTime))
hitObject.FadeOut(editor_hit_object_fade_out_extension).Expire();
}
protected override Playfield CreatePlayfield() => new OsuPlayfieldNoCursor();

View File

@ -72,6 +72,9 @@ namespace osu.Game.Rulesets.Osu.Replays
public override Replay Generate()
{
if (Beatmap.HitObjects.Count == 0)
return Replay;
buttonIndex = 0;
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.
using osu.Framework.Allocation;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
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)
: 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]
private void load()
{
Result.Type = type;
Result.Type = Type;
}
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.
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
namespace osu.Game.Rulesets.Taiko.Tests
{
public class DrawableTestStrongHit : DrawableHit
public class DrawableTestStrongHit : DrawableTestHit
{
private readonly HitResult type;
private readonly bool hitBoth;
public DrawableTestStrongHit(double startTime, HitResult type = HitResult.Great, bool hitBoth = true)
@ -20,12 +17,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
{
IsStrong = true,
StartTime = startTime,
})
}, type)
{
// in order to create nested strong hit
HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
this.type = type;
this.hitBoth = hitBoth;
}
@ -33,10 +26,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
{
base.LoadAsyncComplete();
Result.Type = type;
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;

View File

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

View File

@ -6,7 +6,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Scoring;
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.Skinning
@ -29,15 +28,17 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
AddStep("Good", () => SetContents(() => getContentFor(createStrongHit(HitResult.Good, hitBoth))));
}
private Drawable getContentFor(DrawableTaikoHitObject hit)
private Drawable getContentFor(DrawableTestHit hit)
{
return new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
hit,
new HitExplosion(hit)
// the hit needs to be added to hierarchy in order for nested objects to be created correctly.
// 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,
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)
=> new DrawableTestStrongHit(Time.Current, type, hitBoth);
private DrawableTestHit createStrongHit(HitResult type, bool 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.
using System;
using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
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.Drawables;
using osu.Game.Rulesets.Taiko.UI;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Taiko.Tests
{
[TestFixture]
public class TestSceneHits : OsuTestScene
public class TestSceneHits : DrawableTaikoRulesetTestScene
{
private const double default_duration = 3000;
private const float scroll_time = 1000;
@ -32,8 +29,6 @@ namespace osu.Game.Rulesets.Taiko.Tests
protected override double TimePerAction => default_duration * 2;
private readonly Random rng = new Random(1337);
private DrawableTaikoRuleset drawableRuleset;
private Container playfieldContainer;
[BackgroundDependencyLoader]
private void load()
@ -64,35 +59,6 @@ namespace osu.Game.Rulesets.Taiko.Tests
AddStep("Height test 4", () => changePlayfieldSize(4));
AddStep("Height test 5", () => changePlayfieldSize(5));
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)
@ -128,11 +94,11 @@ namespace osu.Game.Rulesets.Taiko.Tests
switch (step)
{
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;
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;
}
}
@ -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) };
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)
@ -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) };
Add(h);
DrawableRuleset.Playfield.Add(h);
((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(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 });
}
private void addMissJudgement()
{
DrawableTestHit h;
Add(h = new DrawableTestHit(new Hit(), HitResult.Miss));
((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = 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 });
}
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)
{
var swell = new Swell
{
StartTime = drawableRuleset.Playfield.Time.Current + scroll_time,
StartTime = DrawableRuleset.Playfield.Time.Current + scroll_time,
Duration = duration,
};
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)
@ -206,7 +172,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
var d = new DrumRoll
{
StartTime = drawableRuleset.Playfield.Time.Current + scroll_time,
StartTime = DrawableRuleset.Playfield.Time.Current + scroll_time,
IsStrong = strong,
Duration = duration,
TickRate = 8,
@ -217,33 +183,33 @@ namespace osu.Game.Rulesets.Taiko.Tests
d.ApplyDefaults(cpi, new BeatmapDifficulty());
drawableRuleset.Playfield.Add(new DrawableDrumRoll(d));
DrawableRuleset.Playfield.Add(new DrawableDrumRoll(d));
}
private void addCentreHit(bool strong)
{
Hit h = new Hit
{
StartTime = drawableRuleset.Playfield.Time.Current + scroll_time,
StartTime = DrawableRuleset.Playfield.Time.Current + scroll_time,
IsStrong = strong
};
h.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
drawableRuleset.Playfield.Add(new DrawableHit(h));
DrawableRuleset.Playfield.Add(new DrawableHit(h));
}
private void addRimHit(bool strong)
{
Hit h = new Hit
{
StartTime = drawableRuleset.Playfield.Time.Current + scroll_time,
StartTime = DrawableRuleset.Playfield.Time.Current + scroll_time,
IsStrong = strong
};
h.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
drawableRuleset.Playfield.Add(new DrawableHit(h));
DrawableRuleset.Playfield.Add(new DrawableHit(h));
}
private class TestStrongNestedHit : DrawableStrongNestedHit

View File

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

View File

@ -27,5 +27,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
base.LoadComplete();
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()
{
if (Beatmap.HitObjects.Count == 0)
return Replay;
bool hitButton = true;
Frames.Add(new TaikoReplayFrame(-100000));

View File

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

View File

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

View File

@ -9,6 +9,7 @@ using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
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;
addExplosion(judgedObject, type);
addExplosion(judgedObject, result.Type, type);
break;
}
}
@ -219,9 +219,9 @@ namespace osu.Game.Rulesets.Taiko.UI
/// 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.
/// </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)
kiaiExplosionContainer.Add(new KiaiHitExplosion(drawableObject, type));
});

View File

@ -2,7 +2,9 @@
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit;
namespace osu.Game.Tests.Editing
@ -13,11 +15,12 @@ namespace osu.Game.Tests.Editing
[Test]
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.CanRedo.Value, Is.False);
addArbitraryChange(beatmap);
handler.SaveState();
Assert.That(handler.CanUndo.Value, Is.True);
@ -29,15 +32,48 @@ namespace osu.Game.Tests.Editing
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]
public void TestMaxStatesSaved()
{
var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap()));
var (handler, beatmap) = createChangeHandler();
Assert.That(handler.CanUndo.Value, Is.False);
for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES; i++)
{
addArbitraryChange(beatmap);
handler.SaveState();
}
Assert.That(handler.CanUndo.Value, Is.True);
@ -53,12 +89,15 @@ namespace osu.Game.Tests.Editing
[Test]
public void TestMaxStatesExceeded()
{
var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap()));
var (handler, beatmap) = createChangeHandler();
Assert.That(handler.CanUndo.Value, Is.False);
for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES * 2; i++)
{
addArbitraryChange(beatmap);
handler.SaveState();
}
Assert.That(handler.CanUndo.Value, Is.True);
@ -70,5 +109,17 @@ namespace osu.Game.Tests.Editing
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.
// See the LICENCE file in the repository root for full licence text.
using System.Threading;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Utils;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
using osu.Game.Tests.Visual;
@ -175,6 +178,24 @@ namespace osu.Game.Tests.Gameplay
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)
{
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;
public override void SetUpSteps()
{
base.SetUpSteps();
AddUntilStep("gameplay has started",
() => Player.GameplayClockContainer.GameplayClock.CurrentTime > Player.DrawableRuleset.GameplayStartTime);
}
[Test]
public void TestGameplayOverlayActivation()
{
@ -21,7 +29,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
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());
AddUntilStep("activation mode is user triggered", () => Player.OverlayActivationMode == OverlayActivation.UserTriggered);
}

View File

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

View File

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

View File

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

View File

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

View File

@ -151,8 +151,11 @@ namespace osu.Game.Rulesets.UI
public virtual PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new PlayfieldAdjustmentContainer();
[Resolved]
private OsuConfigManager config { get; set; }
[BackgroundDependencyLoader]
private void load(OsuConfigManager config, CancellationToken? cancellationToken)
private void load(CancellationToken? cancellationToken)
{
InternalChildren = new Drawable[]
{
@ -178,11 +181,18 @@ namespace osu.Game.Rulesets.UI
.WithChild(ResumeOverlay)));
}
applyRulesetMods(Mods, config);
RegenerateAutoplay();
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>
/// Creates and adds drawable representations of hit objects to the play field.
/// </summary>

View File

@ -126,39 +126,63 @@ namespace osu.Game.Rulesets.UI
try
{
if (!FrameStablePlayback)
return;
if (firstConsumption)
if (FrameStablePlayback)
{
// On the first update, frame-stability seeking would result in unexpected/unwanted behaviour.
// Instead we perform an initial seek to the proposed time.
if (firstConsumption)
{
// On the first update, frame-stability seeking would result in unexpected/unwanted behaviour.
// Instead we perform an initial seek to the proposed time.
// process frame (in addition to finally clause) to clear out ElapsedTime
manualClock.CurrentTime = newProposedTime;
framedClock.ProcessFrame();
// process frame (in addition to finally clause) to clear out ElapsedTime
manualClock.CurrentTime = newProposedTime;
framedClock.ProcessFrame();
firstConsumption = false;
}
else if (manualClock.CurrentTime < gameplayStartTime)
manualClock.CurrentTime = newProposedTime = Math.Min(gameplayStartTime, newProposedTime);
else if (Math.Abs(manualClock.CurrentTime - newProposedTime) > sixty_frame_time * 1.2f)
{
newProposedTime = newProposedTime > manualClock.CurrentTime
? Math.Min(newProposedTime, manualClock.CurrentTime + sixty_frame_time)
: Math.Max(newProposedTime, manualClock.CurrentTime - sixty_frame_time);
firstConsumption = false;
}
else if (manualClock.CurrentTime < gameplayStartTime)
manualClock.CurrentTime = newProposedTime = Math.Min(gameplayStartTime, newProposedTime);
else if (Math.Abs(manualClock.CurrentTime - newProposedTime) > sixty_frame_time * 1.2f)
{
newProposedTime = newProposedTime > manualClock.CurrentTime
? Math.Min(newProposedTime, manualClock.CurrentTime + sixty_frame_time)
: Math.Max(newProposedTime, manualClock.CurrentTime - sixty_frame_time);
}
}
if (isAttached)
{
double? newTime = ReplayInputHandler.SetFrameFromTime(newProposedTime);
double? newTime;
if (newTime == null)
if (FrameStablePlayback)
{
// we shouldn't execute for this time value. probably waiting on more replay data.
validState = false;
requireMoreUpdateLoops = true;
return;
// 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)
{
// special case for when the replay actually can't arrive at the required time.
// protects from potential endless loop.
validState = false;
return;
}
}
}
newProposedTime = newTime.Value;

View File

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

View File

@ -212,6 +212,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
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;
// 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;
if (comboInfo == null)
continue;
if (comboInfo == null || comboInfo.NewCombo == state) continue;
comboInfo.NewCombo = state;
EditorBeatmap?.UpdateHitObject(h);

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
@ -89,23 +90,27 @@ namespace osu.Game.Screens.Edit
if (isRestoring)
return;
if (currentState < savedStates.Count - 1)
savedStates.RemoveRange(currentState + 1, savedStates.Count - currentState - 1);
if (savedStates.Count > MAX_SAVED_STATES)
savedStates.RemoveAt(0);
using (var stream = new MemoryStream())
{
using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
new LegacyBeatmapEncoder(editorBeatmap, editorBeatmap.BeatmapSkin).Encode(sw);
savedStates.Add(stream.ToArray());
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)
savedStates.RemoveRange(currentState + 1, savedStates.Count - currentState - 1);
if (savedStates.Count > MAX_SAVED_STATES)
savedStates.RemoveAt(0);
savedStates.Add(newState);
currentState = savedStates.Count - 1;
updateBindables();
}
currentState = savedStates.Count - 1;
updateBindables();
}
/// <summary>