Merge branch 'master' into scoring-standardisation

This commit is contained in:
smoogipoo 2020-09-29 15:29:32 +09:00
commit bad48d6d44
66 changed files with 1024 additions and 237 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

@ -16,7 +16,7 @@ using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Tests namespace osu.Game.Rulesets.Mania.Tests.Editor
{ {
public abstract class ManiaPlacementBlueprintTestScene : PlacementBlueprintTestScene public abstract class ManiaPlacementBlueprintTestScene : PlacementBlueprintTestScene
{ {

View File

@ -8,7 +8,7 @@ using osu.Game.Rulesets.Mania.UI;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Tests namespace osu.Game.Rulesets.Mania.Tests.Editor
{ {
public abstract class ManiaSelectionBlueprintTestScene : SelectionBlueprintTestScene public abstract class ManiaSelectionBlueprintTestScene : SelectionBlueprintTestScene
{ {

View File

@ -8,7 +8,7 @@ using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mania.UI;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests namespace osu.Game.Rulesets.Mania.Tests.Editor
{ {
[TestFixture] [TestFixture]
public class TestSceneEditor : EditorTestScene public class TestSceneEditor : EditorTestScene

View File

@ -8,7 +8,7 @@ using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Mania.Tests namespace osu.Game.Rulesets.Mania.Tests.Editor
{ {
public class TestSceneHoldNotePlacementBlueprint : ManiaPlacementBlueprintTestScene public class TestSceneHoldNotePlacementBlueprint : ManiaPlacementBlueprintTestScene
{ {

View File

@ -12,7 +12,7 @@ using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests namespace osu.Game.Rulesets.Mania.Tests.Editor
{ {
public class TestSceneHoldNoteSelectionBlueprint : ManiaSelectionBlueprintTestScene public class TestSceneHoldNoteSelectionBlueprint : ManiaSelectionBlueprintTestScene
{ {

View File

@ -20,7 +20,7 @@ using osu.Game.Screens.Edit;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Mania.Tests namespace osu.Game.Rulesets.Mania.Tests.Editor
{ {
public class TestSceneManiaBeatSnapGrid : EditorClockTestScene public class TestSceneManiaBeatSnapGrid : EditorClockTestScene
{ {

View File

@ -23,7 +23,7 @@ using osu.Game.Tests.Visual;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Rulesets.Mania.Tests namespace osu.Game.Rulesets.Mania.Tests.Editor
{ {
public class TestSceneManiaHitObjectComposer : EditorClockTestScene public class TestSceneManiaHitObjectComposer : EditorClockTestScene
{ {

View File

@ -18,7 +18,7 @@ using osu.Game.Tests.Visual;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Rulesets.Mania.Tests namespace osu.Game.Rulesets.Mania.Tests.Editor
{ {
public class TestSceneNotePlacementBlueprint : ManiaPlacementBlueprintTestScene public class TestSceneNotePlacementBlueprint : ManiaPlacementBlueprintTestScene
{ {

View File

@ -12,7 +12,7 @@ using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Mania.Tests namespace osu.Game.Rulesets.Mania.Tests.Editor
{ {
public class TestSceneNoteSelectionBlueprint : ManiaSelectionBlueprintTestScene public class TestSceneNoteSelectionBlueprint : ManiaSelectionBlueprintTestScene
{ {

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

@ -9,7 +9,7 @@ using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Osu.Tests namespace osu.Game.Rulesets.Osu.Tests.Editor
{ {
public class TestSceneHitCirclePlacementBlueprint : PlacementBlueprintTestScene public class TestSceneHitCirclePlacementBlueprint : PlacementBlueprintTestScene
{ {

View File

@ -11,7 +11,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Osu.Tests namespace osu.Game.Rulesets.Osu.Tests.Editor
{ {
public class TestSceneHitCircleSelectionBlueprint : SelectionBlueprintTestScene public class TestSceneHitCircleSelectionBlueprint : SelectionBlueprintTestScene
{ {

View File

@ -19,7 +19,7 @@ using osu.Game.Tests.Visual;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Tests namespace osu.Game.Rulesets.Osu.Tests.Editor
{ {
public class TestSceneOsuDistanceSnapGrid : OsuManualInputManagerTestScene public class TestSceneOsuDistanceSnapGrid : OsuManualInputManagerTestScene
{ {

View File

@ -12,7 +12,7 @@ using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Osu.Tests namespace osu.Game.Rulesets.Osu.Tests.Editor
{ {
public class TestScenePathControlPointVisualiser : OsuTestScene public class TestScenePathControlPointVisualiser : OsuTestScene
{ {

View File

@ -14,7 +14,7 @@ using osu.Game.Tests.Visual;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Tests namespace osu.Game.Rulesets.Osu.Tests.Editor
{ {
public class TestSceneSliderPlacementBlueprint : PlacementBlueprintTestScene public class TestSceneSliderPlacementBlueprint : PlacementBlueprintTestScene
{ {

View File

@ -16,7 +16,7 @@ using osu.Game.Tests.Visual;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Tests namespace osu.Game.Rulesets.Osu.Tests.Editor
{ {
public class TestSceneSliderSelectionBlueprint : SelectionBlueprintTestScene public class TestSceneSliderSelectionBlueprint : SelectionBlueprintTestScene
{ {

View File

@ -9,7 +9,7 @@ using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Osu.Tests namespace osu.Game.Rulesets.Osu.Tests.Editor
{ {
public class TestSceneSpinnerPlacementBlueprint : PlacementBlueprintTestScene public class TestSceneSpinnerPlacementBlueprint : PlacementBlueprintTestScene
{ {

View File

@ -11,7 +11,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Osu.Tests namespace osu.Game.Rulesets.Osu.Tests.Editor
{ {
public class TestSceneSpinnerSelectionBlueprint : SelectionBlueprintTestScene public class TestSceneSpinnerSelectionBlueprint : SelectionBlueprintTestScene
{ {

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,20 +33,37 @@ 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:
// Get the existing fade out transform // there are quite a few drawable hit types we don't want to extent (spinners, ticks etc.)
var existing = hitObject.Transforms.LastOrDefault(t => t.TargetMember == nameof(Alpha)); return;
if (existing == null)
return;
hitObject.RemoveTransform(existing); case DrawableSlider _:
// no specifics to sliders but let them fade slower below.
break;
using (hitObject.BeginAbsoluteSequence(existing.StartTime)) case DrawableHitCircle circle: // also handles slider heads
hitObject.FadeOut(editor_hit_object_fade_out_extension).Expire(); circle.ApproachCircle
.FadeOutFromOne(editor_hit_object_fade_out_extension)
.Expire();
break; 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(); protected override Playfield CreatePlayfield() => new OsuPlayfieldNoCursor();

View File

@ -9,7 +9,9 @@ using osu.Framework.Bindables;
using osu.Framework.Caching; using osu.Framework.Caching;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@ -17,6 +19,7 @@ using osu.Game.Rulesets.Objects;
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.UI; using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit.Components.TernaryButtons;
using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components;
using osuTK; using osuTK;
@ -39,11 +42,11 @@ namespace osu.Game.Rulesets.Osu.Edit
new SpinnerCompositionTool() new SpinnerCompositionTool()
}; };
private readonly BindableBool distanceSnapToggle = new BindableBool(true) { Description = "Distance Snap" }; private readonly Bindable<TernaryState> distanceSnapToggle = new Bindable<TernaryState>();
protected override IEnumerable<Bindable<bool>> Toggles => base.Toggles.Concat(new[] protected override IEnumerable<TernaryButton> CreateTernaryButtons() => base.CreateTernaryButtons().Concat(new[]
{ {
distanceSnapToggle new TernaryButton(distanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler })
}); });
private BindableList<HitObject> selectedHitObjects; private BindableList<HitObject> selectedHitObjects;
@ -156,7 +159,7 @@ namespace osu.Game.Rulesets.Osu.Edit
distanceSnapGridCache.Invalidate(); distanceSnapGridCache.Invalidate();
distanceSnapGrid = null; distanceSnapGrid = null;
if (!distanceSnapToggle.Value) if (distanceSnapToggle.Value != TernaryState.True)
return; return;
switch (BlueprintContainer.CurrentTool) switch (BlueprintContainer.CurrentTool)

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

@ -4,7 +4,7 @@
using NUnit.Framework; using NUnit.Framework;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Taiko.Tests namespace osu.Game.Rulesets.Taiko.Tests.Editor
{ {
[TestFixture] [TestFixture]
public class TestSceneEditor : EditorTestScene public class TestSceneEditor : EditorTestScene

View File

@ -12,7 +12,7 @@ using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Taiko.Tests namespace osu.Game.Rulesets.Taiko.Tests.Editor
{ {
public class TestSceneTaikoHitObjectComposer : EditorClockTestScene public class TestSceneTaikoHitObjectComposer : EditorClockTestScene
{ {

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)
h.IsStrong = state; {
if (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

@ -0,0 +1,69 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.IO;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit.Setup;
using osu.Game.Tests.Resources;
using SharpCompress.Archives;
using SharpCompress.Archives.Zip;
namespace osu.Game.Tests.Visual.Editing
{
public class TestSceneEditorBeatmapCreation : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
protected override bool EditorComponentsReady => Editor.ChildrenOfType<SetupScreen>().SingleOrDefault()?.IsLoaded == true;
public override void SetUpSteps()
{
AddStep("set dummy", () => Beatmap.Value = new DummyWorkingBeatmap(Audio, null));
base.SetUpSteps();
// if we save a beatmap with a hash collision, things fall over.
// probably needs a more solid resolution in the future but this will do for now.
AddStep("make new beatmap unique", () => EditorBeatmap.Metadata.Title = Guid.NewGuid().ToString());
}
[Test]
public void TestCreateNewBeatmap()
{
AddStep("save beatmap", () => Editor.Save());
AddAssert("new beatmap persisted", () => EditorBeatmap.BeatmapInfo.ID > 0);
}
[Test]
public void TestAddAudioTrack()
{
AddAssert("switch track to real track", () =>
{
var setup = Editor.ChildrenOfType<SetupScreen>().First();
var temp = TestResources.GetTestBeatmapForImport();
string extractedFolder = $"{temp}_extracted";
Directory.CreateDirectory(extractedFolder);
using (var zip = ZipArchive.Open(temp))
zip.WriteToDirectory(extractedFolder);
bool success = setup.ChangeAudioTrack(Path.Combine(extractedFolder, "03. Renatus - Soleily 192kbps.mp3"));
File.Delete(temp);
Directory.Delete(extractedFolder, true);
return success;
});
AddAssert("track length changed", () => Beatmap.Value.Track.Length > 60000);
}
}
}

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

@ -17,6 +17,11 @@ namespace osu.Game.Audio
public const string HIT_NORMAL = @"hitnormal"; public const string HIT_NORMAL = @"hitnormal";
public const string HIT_CLAP = @"hitclap"; public const string HIT_CLAP = @"hitclap";
/// <summary>
/// All valid sample addition constants.
/// </summary>
public static IEnumerable<string> AllAdditions => new[] { HIT_WHISTLE, HIT_CLAP, HIT_FINISH };
/// <summary> /// <summary>
/// The bank to load the sample from. /// The bank to load the sample from.
/// </summary> /// </summary>

View File

@ -260,7 +260,7 @@ namespace osu.Game.Beatmaps
fileInfo.Filename = beatmapInfo.Path; fileInfo.Filename = beatmapInfo.Path;
stream.Seek(0, SeekOrigin.Begin); stream.Seek(0, SeekOrigin.Begin);
UpdateFile(setInfo, fileInfo, stream); ReplaceFile(setInfo, fileInfo, stream);
} }
} }

View File

@ -401,12 +401,27 @@ namespace osu.Game.Database
} }
/// <summary> /// <summary>
/// Update an existing file, or create a new entry if not already part of the <paramref name="model"/>'s files. /// Replace an existing file with a new version.
/// </summary> /// </summary>
/// <param name="model">The item to operate on.</param> /// <param name="model">The item to operate on.</param>
/// <param name="file">The file model to be updated or added.</param> /// <param name="file">The existing file to be replaced.</param>
/// <param name="contents">The new file contents.</param> /// <param name="contents">The new file contents.</param>
public void UpdateFile(TModel model, TFileModel file, Stream contents) /// <param name="filename">An optional filename for the new file. Will use the previous filename if not specified.</param>
public void ReplaceFile(TModel model, TFileModel file, Stream contents, string filename = null)
{
using (ContextFactory.GetForWrite())
{
DeleteFile(model, file);
AddFile(model, contents, filename ?? file.Filename);
}
}
/// <summary>
/// Delete new file.
/// </summary>
/// <param name="model">The item to operate on.</param>
/// <param name="file">The existing file to be deleted.</param>
public void DeleteFile(TModel model, TFileModel file)
{ {
using (var usage = ContextFactory.GetForWrite()) using (var usage = ContextFactory.GetForWrite())
{ {
@ -415,15 +430,28 @@ namespace osu.Game.Database
{ {
Files.Dereference(file.FileInfo); Files.Dereference(file.FileInfo);
// Remove the file model. // This shouldn't be required, but here for safety in case the provided TModel is not being change tracked
// Definitely can be removed once we rework the database backend.
usage.Context.Set<TFileModel>().Remove(file); usage.Context.Set<TFileModel>().Remove(file);
} }
// Add the new file info and containing file model.
model.Files.Remove(file); model.Files.Remove(file);
}
}
/// <summary>
/// Add a new file.
/// </summary>
/// <param name="model">The item to operate on.</param>
/// <param name="contents">The new file contents.</param>
/// <param name="filename">The filename for the new file.</param>
public void AddFile(TModel model, Stream contents, string filename)
{
using (ContextFactory.GetForWrite())
{
model.Files.Add(new TFileModel model.Files.Add(new TFileModel
{ {
Filename = file.Filename, Filename = filename,
FileInfo = Files.Add(contents) FileInfo = Files.Add(contents)
}); });

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

@ -44,13 +44,18 @@ namespace osu.Game.Graphics.UserInterfaceV2
Component.BorderColour = colours.Blue; Component.BorderColour = colours.Blue;
} }
protected override OsuTextBox CreateComponent() => new OsuTextBox protected virtual OsuTextBox CreateTextBox() => new OsuTextBox
{ {
CommitOnFocusLost = true, CommitOnFocusLost = true,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
CornerRadius = CORNER_RADIUS, CornerRadius = CORNER_RADIUS,
}.With(t => t.OnCommit += (sender, newText) => OnCommit?.Invoke(sender, newText)); };
protected override OsuTextBox CreateComponent() => CreateTextBox().With(t =>
{
t.OnCommit += (sender, newText) => OnCommit?.Invoke(sender, newText);
});
} }
} }

View File

@ -81,6 +81,11 @@ namespace osu.Game.Overlays
mods.BindValueChanged(_ => ResetTrackAdjustments(), true); mods.BindValueChanged(_ => ResetTrackAdjustments(), true);
} }
/// <summary>
/// Forcefully reload the current <see cref="WorkingBeatmap"/>'s track from disk.
/// </summary>
public void ReloadCurrentTrack() => changeTrack();
/// <summary> /// <summary>
/// Change the position of a <see cref="BeatmapSetInfo"/> in the current playlist. /// Change the position of a <see cref="BeatmapSetInfo"/> in the current playlist.
/// </summary> /// </summary>

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

@ -6,7 +6,6 @@ using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input; using osu.Framework.Input;
@ -14,7 +13,6 @@ using osu.Framework.Input.Events;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Configuration;
using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@ -24,6 +22,7 @@ using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components.RadioButtons; using osu.Game.Screens.Edit.Components.RadioButtons;
using osu.Game.Screens.Edit.Components.TernaryButtons;
using osu.Game.Screens.Edit.Compose; using osu.Game.Screens.Edit.Compose;
using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components;
using osuTK; using osuTK;
@ -63,7 +62,7 @@ namespace osu.Game.Rulesets.Edit
private RadioButtonCollection toolboxCollection; private RadioButtonCollection toolboxCollection;
private ToolboxGroup togglesCollection; private FillFlowContainer togglesCollection;
protected HitObjectComposer(Ruleset ruleset) protected HitObjectComposer(Ruleset ruleset)
{ {
@ -77,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
@ -121,14 +120,19 @@ namespace osu.Game.Rulesets.Edit
Spacing = new Vector2(10), Spacing = new Vector2(10),
Children = new Drawable[] Children = new Drawable[]
{ {
new ToolboxGroup("toolbox") { Child = toolboxCollection = new RadioButtonCollection { RelativeSizeAxes = Axes.X } }, new ToolboxGroup("toolbox (1-9)")
togglesCollection = new ToolboxGroup("toggles")
{ {
ChildrenEnumerable = Toggles.Select(b => new SettingsCheckbox Child = toolboxCollection = new RadioButtonCollection { RelativeSizeAxes = Axes.X }
},
new ToolboxGroup("toggles (Q~P)")
{
Child = togglesCollection = new FillFlowContainer
{ {
Bindable = b, RelativeSizeAxes = Axes.X,
LabelText = b?.Description ?? "unknown" AutoSizeAxes = Axes.Y,
}) Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 5),
},
} }
} }
}, },
@ -139,6 +143,9 @@ namespace osu.Game.Rulesets.Edit
.Select(t => new RadioButton(t.Name, () => toolSelected(t), t.CreateIcon)) .Select(t => new RadioButton(t.Name, () => toolSelected(t), t.CreateIcon))
.ToList(); .ToList();
TernaryStates = CreateTernaryButtons().ToArray();
togglesCollection.AddRange(TernaryStates.Select(b => new DrawableTernaryButton(b)));
setSelectTool(); setSelectTool();
EditorBeatmap.SelectedHitObjects.CollectionChanged += selectionChanged; EditorBeatmap.SelectedHitObjects.CollectionChanged += selectionChanged;
@ -167,10 +174,14 @@ namespace osu.Game.Rulesets.Edit
protected abstract IReadOnlyList<HitObjectCompositionTool> CompositionTools { get; } protected abstract IReadOnlyList<HitObjectCompositionTool> CompositionTools { get; }
/// <summary> /// <summary>
/// A collection of toggles which will be displayed to the user. /// A collection of states which will be displayed to the user in the toolbox.
/// The display name will be decided by <see cref="Bindable{T}.Description"/>.
/// </summary> /// </summary>
protected virtual IEnumerable<Bindable<bool>> Toggles => BlueprintContainer.Toggles; public TernaryButton[] TernaryStates { get; private set; }
/// <summary>
/// Create all ternary states required to be displayed to the user.
/// </summary>
protected virtual IEnumerable<TernaryButton> CreateTernaryButtons() => BlueprintContainer.TernaryStates;
/// <summary> /// <summary>
/// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic. /// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic.
@ -215,9 +226,9 @@ namespace osu.Game.Rulesets.Edit
{ {
var item = togglesCollection.ElementAtOrDefault(rightIndex); var item = togglesCollection.ElementAtOrDefault(rightIndex);
if (item is SettingsCheckbox checkbox) if (item is DrawableTernaryButton button)
{ {
checkbox.Bindable.Value = !checkbox.Bindable.Value; button.Button.Toggle();
return true; return true;
} }
} }

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

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

View File

@ -18,7 +18,8 @@ namespace osu.Game.Screens.Edit.Components
private const float contents_padding = 15; private const float contents_padding = 15;
protected readonly IBindable<WorkingBeatmap> Beatmap = new Bindable<WorkingBeatmap>(); protected readonly IBindable<WorkingBeatmap> Beatmap = new Bindable<WorkingBeatmap>();
protected Track Track => Beatmap.Value.Track;
protected readonly IBindable<Track> Track = new Bindable<Track>();
private readonly Drawable background; private readonly Drawable background;
private readonly Container content; private readonly Container content;
@ -42,9 +43,11 @@ namespace osu.Game.Screens.Edit.Components
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(IBindable<WorkingBeatmap> beatmap, OsuColour colours) private void load(IBindable<WorkingBeatmap> beatmap, OsuColour colours, EditorClock clock)
{ {
Beatmap.BindTo(beatmap); Beatmap.BindTo(beatmap);
Track.BindTo(clock.Track);
background.Colour = colours.Gray1; background.Colour = colours.Gray1;
} }
} }

View File

@ -62,12 +62,12 @@ namespace osu.Game.Screens.Edit.Components
} }
}; };
Track?.AddAdjustment(AdjustableProperty.Tempo, tempo); Track.BindValueChanged(tr => tr.NewValue?.AddAdjustment(AdjustableProperty.Tempo, tempo), true);
} }
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
Track?.RemoveAdjustment(AdjustableProperty.Tempo, tempo); Track.Value?.RemoveAdjustment(AdjustableProperty.Tempo, tempo);
base.Dispose(isDisposing); base.Dispose(isDisposing);
} }

View File

@ -0,0 +1,112 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Components.TernaryButtons
{
internal class DrawableTernaryButton : TriangleButton
{
private Color4 defaultBackgroundColour;
private Color4 defaultBubbleColour;
private Color4 selectedBackgroundColour;
private Color4 selectedBubbleColour;
private Drawable icon;
public readonly TernaryButton Button;
public DrawableTernaryButton(TernaryButton button)
{
Button = button;
Text = button.Description;
RelativeSizeAxes = Axes.X;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
defaultBackgroundColour = colours.Gray3;
defaultBubbleColour = defaultBackgroundColour.Darken(0.5f);
selectedBackgroundColour = colours.BlueDark;
selectedBubbleColour = selectedBackgroundColour.Lighten(0.5f);
Triangles.Alpha = 0;
Content.EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Radius = 2,
Offset = new Vector2(0, 1),
Colour = Color4.Black.Opacity(0.5f)
};
Add(icon = (Button.CreateIcon?.Invoke() ?? new Circle()).With(b =>
{
b.Blending = BlendingParameters.Additive;
b.Anchor = Anchor.CentreLeft;
b.Origin = Anchor.CentreLeft;
b.Size = new Vector2(20);
b.X = 10;
}));
}
protected override void LoadComplete()
{
base.LoadComplete();
Button.Bindable.BindValueChanged(selected => updateSelectionState(), true);
Action = onAction;
}
private void onAction()
{
Button.Toggle();
}
private void updateSelectionState()
{
if (!IsLoaded)
return;
switch (Button.Bindable.Value)
{
case TernaryState.Indeterminate:
icon.Colour = selectedBubbleColour.Darken(0.5f);
BackgroundColour = selectedBackgroundColour.Darken(0.5f);
break;
case TernaryState.False:
icon.Colour = defaultBubbleColour;
BackgroundColour = defaultBackgroundColour;
break;
case TernaryState.True:
icon.Colour = selectedBubbleColour;
BackgroundColour = selectedBackgroundColour;
break;
}
}
protected override SpriteText CreateText() => new OsuSpriteText
{
Depth = -1,
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
X = 40f
};
}
}

View File

@ -0,0 +1,44 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Screens.Edit.Components.TernaryButtons
{
public class TernaryButton
{
public readonly Bindable<TernaryState> Bindable;
public readonly string Description;
/// <summary>
/// A function which creates a drawable icon to represent this item. If null, a sane default should be used.
/// </summary>
public readonly Func<Drawable> CreateIcon;
public TernaryButton(Bindable<TernaryState> bindable, string description, Func<Drawable> createIcon = null)
{
Bindable = bindable;
Description = description;
CreateIcon = createIcon;
}
public void Toggle()
{
switch (Bindable.Value)
{
case TernaryState.False:
case TernaryState.Indeterminate:
Bindable.Value = TernaryState.True;
break;
case TernaryState.True:
Bindable.Value = TernaryState.False;
break;
}
}
}
}

View File

@ -3,6 +3,7 @@
using System; using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osuTK; using osuTK;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -22,6 +23,8 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
{ {
protected readonly IBindable<WorkingBeatmap> Beatmap = new Bindable<WorkingBeatmap>(); protected readonly IBindable<WorkingBeatmap> Beatmap = new Bindable<WorkingBeatmap>();
protected readonly IBindable<Track> Track = new Bindable<Track>();
private readonly Container<T> content; private readonly Container<T> content;
protected override Container<T> Content => content; protected override Container<T> Content => content;
@ -35,12 +38,15 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
updateRelativeChildSize(); updateRelativeChildSize();
LoadBeatmap(b.NewValue); LoadBeatmap(b.NewValue);
}; };
Track.ValueChanged += _ => updateRelativeChildSize();
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(IBindable<WorkingBeatmap> beatmap) private void load(IBindable<WorkingBeatmap> beatmap, EditorClock clock)
{ {
Beatmap.BindTo(beatmap); Beatmap.BindTo(beatmap);
Track.BindTo(clock.Track);
} }
private void updateRelativeChildSize() private void updateRelativeChildSize()

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

@ -3,16 +3,21 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Humanizer;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Game.Audio;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Screens.Edit.Components.TernaryButtons;
using osuTK; using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components namespace osu.Game.Screens.Edit.Compose.Components
@ -48,6 +53,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
TernaryStates = CreateTernaryButtons().ToArray();
AddInternal(placementBlueprintContainer); AddInternal(placementBlueprintContainer);
} }
@ -57,36 +64,92 @@ namespace osu.Game.Screens.Edit.Compose.Components
inputManager = GetContainingInputManager(); inputManager = GetContainingInputManager();
Beatmap.SelectedHitObjects.CollectionChanged += (_, __) => updateTogglesFromSelection(); // updates to selected are handled for us by SelectionHandler.
NewCombo.BindTo(SelectionHandler.SelectionNewComboState);
// the updated object may be in the selection // we are responsible for current placement blueprint updated based on state changes.
Beatmap.HitObjectUpdated += _ => updateTogglesFromSelection(); NewCombo.ValueChanged += _ => updatePlacementNewCombo();
NewCombo.ValueChanged += combo => // we own SelectionHandler so don't need to worry about making bindable copies (for simplicity)
foreach (var kvp in SelectionHandler.SelectionSampleStates)
{ {
if (Beatmap.SelectedHitObjects.Count > 0) kvp.Value.BindValueChanged(_ => updatePlacementSamples());
{ }
SelectionHandler.SetNewCombo(combo.NewValue);
}
else if (currentPlacement != null)
{
// update placement object from toggle
if (currentPlacement.HitObject is IHasComboInformation c)
c.NewCombo = combo.NewValue;
}
};
} }
private void updateTogglesFromSelection() => private void updatePlacementNewCombo()
NewCombo.Value = Beatmap.SelectedHitObjects.OfType<IHasComboInformation>().All(c => c.NewCombo); {
if (currentPlacement == null) return;
public readonly Bindable<bool> NewCombo = new Bindable<bool> { Description = "New Combo" }; if (currentPlacement.HitObject is IHasComboInformation c)
c.NewCombo = NewCombo.Value == TernaryState.True;
}
public virtual IEnumerable<Bindable<bool>> Toggles => new[] private void updatePlacementSamples()
{
if (currentPlacement == null) return;
foreach (var kvp in SelectionHandler.SelectionSampleStates)
sampleChanged(kvp.Key, kvp.Value.Value);
}
private void sampleChanged(string sampleName, TernaryState state)
{
if (currentPlacement == null) return;
var samples = currentPlacement.HitObject.Samples;
var existingSample = samples.FirstOrDefault(s => s.Name == sampleName);
switch (state)
{
case TernaryState.False:
if (existingSample != null)
samples.Remove(existingSample);
break;
case TernaryState.True:
if (existingSample == null)
samples.Add(new HitSampleInfo { Name = sampleName });
break;
}
}
public readonly Bindable<TernaryState> NewCombo = new Bindable<TernaryState> { Description = "New Combo" };
/// <summary>
/// A collection of states which will be displayed to the user in the toolbox.
/// </summary>
public TernaryButton[] TernaryStates { get; private set; }
/// <summary>
/// Create all ternary states required to be displayed to the user.
/// </summary>
protected virtual IEnumerable<TernaryButton> CreateTernaryButtons()
{ {
//TODO: this should only be enabled (visible?) for rulesets that provide combo-supporting HitObjects. //TODO: this should only be enabled (visible?) for rulesets that provide combo-supporting HitObjects.
NewCombo yield return new TernaryButton(NewCombo, "New combo", () => new SpriteIcon { Icon = FontAwesome.Regular.DotCircle });
};
foreach (var kvp in SelectionHandler.SelectionSampleStates)
yield return new TernaryButton(kvp.Value, kvp.Key.Replace("hit", string.Empty).Titleize(), () => getIconForSample(kvp.Key));
}
private Drawable getIconForSample(string sampleName)
{
switch (sampleName)
{
case HitSampleInfo.HIT_CLAP:
return new SpriteIcon { Icon = FontAwesome.Solid.Hands };
case HitSampleInfo.HIT_WHISTLE:
return new SpriteIcon { Icon = FontAwesome.Solid.Bullhorn };
case HitSampleInfo.HIT_FINISH:
return new SpriteIcon { Icon = FontAwesome.Solid.DrumSteelpan };
}
return null;
}
#region Placement #region Placement
@ -149,10 +212,17 @@ 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
updatePlacementPosition(); updatePlacementPosition();
updatePlacementSamples();
updatePlacementNewCombo();
} }
} }

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);
@ -316,19 +315,22 @@ namespace osu.Game.Screens.Edit.Compose.Components
#region Selection State #region Selection State
private readonly Bindable<TernaryState> selectionNewComboState = new Bindable<TernaryState>(); /// <summary>
/// The state of "new combo" for all selected hitobjects.
/// </summary>
public readonly Bindable<TernaryState> SelectionNewComboState = new Bindable<TernaryState>();
private readonly Dictionary<string, Bindable<TernaryState>> selectionSampleStates = new Dictionary<string, Bindable<TernaryState>>(); /// <summary>
/// The state of each sample type for all selected hitobjects. Keys match with <see cref="HitSampleInfo"/> constant specifications.
/// </summary>
public readonly Dictionary<string, Bindable<TernaryState>> SelectionSampleStates = new Dictionary<string, Bindable<TernaryState>>();
/// <summary> /// <summary>
/// Set up ternary state bindables and bind them to selection/hitobject changes (in both directions) /// Set up ternary state bindables and bind them to selection/hitobject changes (in both directions)
/// </summary> /// </summary>
private void createStateBindables() private void createStateBindables()
{ {
// hit samples foreach (var sampleName in HitSampleInfo.AllAdditions)
var sampleTypes = new[] { HitSampleInfo.HIT_WHISTLE, HitSampleInfo.HIT_CLAP, HitSampleInfo.HIT_FINISH };
foreach (var sampleName in sampleTypes)
{ {
var bindable = new Bindable<TernaryState> var bindable = new Bindable<TernaryState>
{ {
@ -349,11 +351,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
} }
}; };
selectionSampleStates[sampleName] = bindable; SelectionSampleStates[sampleName] = bindable;
} }
// new combo // new combo
selectionNewComboState.ValueChanged += state => SelectionNewComboState.ValueChanged += state =>
{ {
switch (state.NewValue) switch (state.NewValue)
{ {
@ -377,9 +379,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// </summary> /// </summary>
protected virtual void UpdateTernaryStates() protected virtual void UpdateTernaryStates()
{ {
selectionNewComboState.Value = GetStateFromSelection(SelectedHitObjects.OfType<IHasComboInformation>(), h => h.NewCombo); SelectionNewComboState.Value = GetStateFromSelection(SelectedHitObjects.OfType<IHasComboInformation>(), h => h.NewCombo);
foreach (var (sampleName, bindable) in selectionSampleStates) foreach (var (sampleName, bindable) in SelectionSampleStates)
{ {
bindable.Value = GetStateFromSelection(SelectedHitObjects, h => h.Samples.Any(s => s.Name == sampleName)); bindable.Value = GetStateFromSelection(SelectedHitObjects, h => h.Samples.Any(s => s.Name == sampleName));
} }
@ -413,7 +415,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (selectedBlueprints.All(b => b.HitObject is IHasComboInformation)) if (selectedBlueprints.All(b => b.HitObject is IHasComboInformation))
{ {
items.Add(new TernaryStateMenuItem("New combo") { State = { BindTarget = selectionNewComboState } }); items.Add(new TernaryStateMenuItem("New combo") { State = { BindTarget = SelectionNewComboState } });
} }
if (selectedBlueprints.Count == 1) if (selectedBlueprints.Count == 1)
@ -423,7 +425,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
{ {
new OsuMenuItem("Sound") new OsuMenuItem("Sound")
{ {
Items = selectionSampleStates.Select(kvp => Items = SelectionSampleStates.Select(kvp =>
new TernaryStateMenuItem(kvp.Value.Description) { State = { BindTarget = kvp.Value } }).ToArray() new TernaryStateMenuItem(kvp.Value.Description) { State = { BindTarget = kvp.Value } }).ToArray()
}, },
new OsuMenuItem("Delete", MenuItemType.Destructive, deleteSelected), new OsuMenuItem("Delete", MenuItemType.Destructive, deleteSelected),

View File

@ -43,6 +43,7 @@ using osuTK.Input;
namespace osu.Game.Screens.Edit namespace osu.Game.Screens.Edit
{ {
[Cached(typeof(IBeatSnapProvider))] [Cached(typeof(IBeatSnapProvider))]
[Cached]
public class Editor : ScreenWithBeatmapBackground, IKeyBindingHandler<GlobalAction>, IKeyBindingHandler<PlatformAction>, IBeatSnapProvider public class Editor : ScreenWithBeatmapBackground, IKeyBindingHandler<GlobalAction>, IKeyBindingHandler<PlatformAction>, IBeatSnapProvider
{ {
public override float BackgroundParallaxAmount => 0.1f; public override float BackgroundParallaxAmount => 0.1f;
@ -91,6 +92,9 @@ namespace osu.Game.Screens.Edit
[Resolved] [Resolved]
private IAPIProvider api { get; set; } private IAPIProvider api { get; set; }
[Resolved]
private MusicController music { get; set; }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours, GameHost host) private void load(OsuColour colours, GameHost host)
{ {
@ -98,9 +102,9 @@ namespace osu.Game.Screens.Edit
beatDivisor.BindValueChanged(divisor => Beatmap.Value.BeatmapInfo.BeatDivisor = divisor.NewValue); beatDivisor.BindValueChanged(divisor => Beatmap.Value.BeatmapInfo.BeatDivisor = divisor.NewValue);
// Todo: should probably be done at a DrawableRuleset level to share logic with Player. // Todo: should probably be done at a DrawableRuleset level to share logic with Player.
var sourceClock = (IAdjustableClock)Beatmap.Value.Track ?? new StopwatchClock();
clock = new EditorClock(Beatmap.Value, beatDivisor) { IsCoupled = false }; clock = new EditorClock(Beatmap.Value, beatDivisor) { IsCoupled = false };
clock.ChangeSource(sourceClock);
UpdateClockSource();
dependencies.CacheAs(clock); dependencies.CacheAs(clock);
AddInternal(clock); AddInternal(clock);
@ -271,6 +275,15 @@ namespace osu.Game.Screens.Edit
bottomBackground.Colour = colours.Gray2; bottomBackground.Colour = colours.Gray2;
} }
/// <summary>
/// If the beatmap's track has changed, this method must be called to keep the editor in a valid state.
/// </summary>
public void UpdateClockSource()
{
var sourceClock = (IAdjustableClock)Beatmap.Value.Track ?? new StopwatchClock();
clock.ChangeSource(sourceClock);
}
protected void Save() protected void Save()
{ {
// apply any set-level metadata changes. // apply any set-level metadata changes.

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,23 +90,27 @@ namespace osu.Game.Screens.Edit
if (isRestoring) if (isRestoring)
return; 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 stream = new MemoryStream())
{ {
using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
new LegacyBeatmapEncoder(editorBeatmap, editorBeatmap.BeatmapSkin).Encode(sw); 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> /// <summary>

View File

@ -3,6 +3,8 @@
using System; using System;
using System.Linq; using System.Linq;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms; using osu.Framework.Graphics.Transforms;
using osu.Framework.Utils; using osu.Framework.Utils;
@ -17,7 +19,11 @@ namespace osu.Game.Screens.Edit
/// </summary> /// </summary>
public class EditorClock : Component, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock public class EditorClock : Component, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock
{ {
public readonly double TrackLength; public IBindable<Track> Track => track;
private readonly Bindable<Track> track = new Bindable<Track>();
public double TrackLength => track.Value?.Length ?? 60000;
public ControlPointInfo ControlPointInfo; public ControlPointInfo ControlPointInfo;
@ -35,7 +41,6 @@ namespace osu.Game.Screens.Edit
this.beatDivisor = beatDivisor; this.beatDivisor = beatDivisor;
ControlPointInfo = controlPointInfo; ControlPointInfo = controlPointInfo;
TrackLength = trackLength;
underlyingClock = new DecoupleableInterpolatingFramedClock(); underlyingClock = new DecoupleableInterpolatingFramedClock();
} }
@ -190,7 +195,11 @@ namespace osu.Game.Screens.Edit
public FrameTimeInfo TimeInfo => underlyingClock.TimeInfo; public FrameTimeInfo TimeInfo => underlyingClock.TimeInfo;
public void ChangeSource(IClock source) => underlyingClock.ChangeSource(source); public void ChangeSource(IClock source)
{
track.Value = source as Track;
underlyingClock.ChangeSource(source);
}
public IClock Source => underlyingClock.Source; public IClock Source => underlyingClock.Source;

View File

@ -1,17 +1,24 @@
// 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.IO;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables; using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays;
using osuTK; using osuTK;
namespace osu.Game.Screens.Edit.Setup namespace osu.Game.Screens.Edit.Setup
@ -23,6 +30,16 @@ namespace osu.Game.Screens.Edit.Setup
private LabelledTextBox titleTextBox; private LabelledTextBox titleTextBox;
private LabelledTextBox creatorTextBox; private LabelledTextBox creatorTextBox;
private LabelledTextBox difficultyTextBox; private LabelledTextBox difficultyTextBox;
private LabelledTextBox audioTrackTextBox;
[Resolved]
private MusicController music { get; set; }
[Resolved]
private BeatmapManager beatmaps { get; set; }
[Resolved(canBeNull: true)]
private Editor editor { get; set; }
public SetupScreen() public SetupScreen()
: base(EditorScreenMode.SongSetup) : base(EditorScreenMode.SongSetup)
@ -32,6 +49,12 @@ namespace osu.Game.Screens.Edit.Setup
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours)
{ {
Container audioTrackFileChooserContainer = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
};
Child = new Container Child = new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
@ -75,6 +98,18 @@ namespace osu.Game.Screens.Edit.Setup
}, },
}, },
new OsuSpriteText new OsuSpriteText
{
Text = "Resources"
},
audioTrackTextBox = new FileChooserLabelledTextBox
{
Label = "Audio Track",
Current = { Value = Beatmap.Value.Metadata.AudioFile ?? "Click to select a track" },
Target = audioTrackFileChooserContainer,
TabbableContentContainer = this
},
audioTrackFileChooserContainer,
new OsuSpriteText
{ {
Text = "Beatmap metadata" Text = "Beatmap metadata"
}, },
@ -109,10 +144,47 @@ namespace osu.Game.Screens.Edit.Setup
} }
}; };
audioTrackTextBox.Current.BindValueChanged(audioTrackChanged);
foreach (var item in flow.OfType<LabelledTextBox>()) foreach (var item in flow.OfType<LabelledTextBox>())
item.OnCommit += onCommit; item.OnCommit += onCommit;
} }
public bool ChangeAudioTrack(string path)
{
var info = new FileInfo(path);
if (!info.Exists)
return false;
var set = Beatmap.Value.BeatmapSetInfo;
// remove the previous audio track for now.
// in the future we probably want to check if this is being used elsewhere (other difficulties?)
var oldFile = set.Files.FirstOrDefault(f => f.Filename == Beatmap.Value.Metadata.AudioFile);
using (var stream = info.OpenRead())
{
if (oldFile != null)
beatmaps.ReplaceFile(set, oldFile, stream, info.Name);
else
beatmaps.AddFile(set, stream, info.Name);
}
Beatmap.Value.Metadata.AudioFile = info.Name;
music.ReloadCurrentTrack();
editor?.UpdateClockSource();
return true;
}
private void audioTrackChanged(ValueChangedEvent<string> filePath)
{
if (!ChangeAudioTrack(filePath.NewValue))
audioTrackTextBox.Current.Value = filePath.OldValue;
}
private void onCommit(TextBox sender, bool newText) private void onCommit(TextBox sender, bool newText)
{ {
if (!newText) return; if (!newText) return;
@ -125,4 +197,60 @@ namespace osu.Game.Screens.Edit.Setup
Beatmap.Value.BeatmapInfo.Version = difficultyTextBox.Current.Value; Beatmap.Value.BeatmapInfo.Version = difficultyTextBox.Current.Value;
} }
} }
internal class FileChooserLabelledTextBox : LabelledTextBox
{
public Container Target;
private readonly IBindable<FileInfo> currentFile = new Bindable<FileInfo>();
public FileChooserLabelledTextBox()
{
currentFile.BindValueChanged(onFileSelected);
}
private void onFileSelected(ValueChangedEvent<FileInfo> file)
{
if (file.NewValue == null)
return;
Target.Clear();
Current.Value = file.NewValue.FullName;
}
protected override OsuTextBox CreateTextBox() =>
new FileChooserOsuTextBox
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
CornerRadius = CORNER_RADIUS,
OnFocused = DisplayFileChooser
};
public void DisplayFileChooser()
{
Target.Child = new FileSelector(validFileExtensions: new[] { ".mp3", ".ogg" })
{
RelativeSizeAxes = Axes.X,
Height = 400,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
CurrentFile = { BindTarget = currentFile }
};
}
internal class FileChooserOsuTextBox : OsuTextBox
{
public Action OnFocused;
protected override void OnFocus(FocusEvent e)
{
OnFocused?.Invoke();
base.OnFocus(e);
GetContainingInputManager().TriggerFocusContention(this);
}
}
}
} }

View File

@ -26,13 +26,15 @@ namespace osu.Game.Tests.Visual
Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value); Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value);
} }
protected virtual bool EditorComponentsReady => Editor.ChildrenOfType<HitObjectComposer>().FirstOrDefault()?.IsLoaded == true
&& Editor.ChildrenOfType<TimelineArea>().FirstOrDefault()?.IsLoaded == true;
public override void SetUpSteps() public override void SetUpSteps()
{ {
base.SetUpSteps(); base.SetUpSteps();
AddStep("load editor", () => LoadScreen(Editor = CreateEditor())); AddStep("load editor", () => LoadScreen(Editor = CreateEditor()));
AddUntilStep("wait for editor to load", () => Editor.ChildrenOfType<HitObjectComposer>().FirstOrDefault()?.IsLoaded == true AddUntilStep("wait for editor to load", () => EditorComponentsReady);
&& Editor.ChildrenOfType<TimelineArea>().FirstOrDefault()?.IsLoaded == true);
AddStep("get beatmap", () => EditorBeatmap = Editor.ChildrenOfType<EditorBeatmap>().Single()); AddStep("get beatmap", () => EditorBeatmap = Editor.ChildrenOfType<EditorBeatmap>().Single());
AddStep("get clock", () => EditorClock = Editor.ChildrenOfType<EditorClock>().Single()); AddStep("get clock", () => EditorClock = Editor.ChildrenOfType<EditorClock>().Single());
} }

View File

@ -909,6 +909,7 @@ private void load()
<s:Boolean x:Key="/Default/UserDictionary/Words/=beatmaps/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=beatmaps/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=beatmap_0027s/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=beatmap_0027s/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=bindable/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=bindable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=bindables/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Catmull/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Catmull/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Drawables/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Drawables/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=gameplay/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=gameplay/@EntryIndexedValue">True</s:Boolean>