1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-15 06:42:56 +08:00

Merge branch 'master' into catch-hyperdash-catcher-colouring

This commit is contained in:
Dean Herbert 2020-05-14 10:42:49 +09:00 committed by GitHub
commit 7084b748c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 1375 additions and 66 deletions

View File

@ -52,6 +52,6 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.427.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.508.1" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.511.0" />
</ItemGroup>
</Project>

View File

@ -7,6 +7,7 @@ using osu.Framework.Input.Events;
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
using osu.Game.Rulesets.Mania.Objects;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
@ -49,6 +50,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
protected override void OnMouseUp(MouseUpEvent e)
{
if (e.Button != MouseButton.Left)
return;
base.OnMouseUp(e);
EndPlacement(true);
}

View File

@ -11,6 +11,7 @@ using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
@ -46,6 +47,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
protected override bool OnMouseDown(MouseDownEvent e)
{
if (e.Button != MouseButton.Left)
return false;
if (Column == null)
return base.OnMouseDown(e);

View File

@ -5,6 +5,7 @@ using osu.Framework.Graphics;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
using osu.Game.Rulesets.Mania.Objects;
using osuTK.Input;
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
@ -30,6 +31,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
protected override bool OnMouseDown(MouseDownEvent e)
{
if (e.Button != MouseButton.Left)
return false;
base.OnMouseDown(e);
// Place the note immediately.

View File

@ -28,8 +28,11 @@ namespace osu.Game.Rulesets.Osu.Mods
slider.NestedHitObjects.OfType<SliderTick>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
slider.NestedHitObjects.OfType<SliderRepeat>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
foreach (var point in slider.Path.ControlPoints)
var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position.Value, p.Type.Value)).ToArray();
foreach (var point in controlPoints)
point.Position.Value = new Vector2(point.Position.Value.X, -point.Position.Value.Y);
slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value);
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View File

@ -0,0 +1,223 @@
// 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.Collections.Generic;
using System.Linq;
using Humanizer;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Judgements;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Scoring;
using osu.Game.Rulesets.Taiko.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Taiko.Tests.Skinning
{
[TestFixture]
public class TestSceneDrawableTaikoMascot : TaikoSkinnableTestScene
{
public override IReadOnlyList<Type> RequiredTypes => base.RequiredTypes.Concat(new[]
{
typeof(DrawableTaikoMascot),
typeof(TaikoMascotAnimation)
}).ToList();
[Cached(typeof(IScrollingInfo))]
private ScrollingTestContainer.TestScrollingInfo info = new ScrollingTestContainer.TestScrollingInfo
{
Direction = { Value = ScrollingDirection.Left },
TimeRange = { Value = 5000 },
};
private TaikoScoreProcessor scoreProcessor;
private IEnumerable<DrawableTaikoMascot> mascots => this.ChildrenOfType<DrawableTaikoMascot>();
private IEnumerable<TaikoPlayfield> playfields => this.ChildrenOfType<TaikoPlayfield>();
[SetUp]
public void SetUp()
{
scoreProcessor = new TaikoScoreProcessor();
}
[Test]
public void TestStateAnimations()
{
AddStep("set beatmap", () => setBeatmap());
AddStep("clear state", () => SetContents(() => new TaikoMascotAnimation(TaikoMascotAnimationState.Clear)));
AddStep("idle state", () => SetContents(() => new TaikoMascotAnimation(TaikoMascotAnimationState.Idle)));
AddStep("kiai state", () => SetContents(() => new TaikoMascotAnimation(TaikoMascotAnimationState.Kiai)));
AddStep("fail state", () => SetContents(() => new TaikoMascotAnimation(TaikoMascotAnimationState.Fail)));
}
[Test]
public void TestInitialState()
{
AddStep("create mascot", () => SetContents(() => new DrawableTaikoMascot { RelativeSizeAxes = Axes.Both }));
AddAssert("mascot initially idle", () => allMascotsIn(TaikoMascotAnimationState.Idle));
}
[Test]
public void TestClearStateTransition()
{
AddStep("set beatmap", () => setBeatmap());
AddStep("create mascot", () => SetContents(() => new DrawableTaikoMascot { RelativeSizeAxes = Axes.Both }));
AddStep("set clear state", () => mascots.ForEach(mascot => mascot.State.Value = TaikoMascotAnimationState.Clear));
AddStep("miss", () => mascots.ForEach(mascot => mascot.LastResult.Value = new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Miss }));
AddAssert("skins with animations remain in clear state", () => someMascotsIn(TaikoMascotAnimationState.Clear));
AddUntilStep("state reverts to fail", () => allMascotsIn(TaikoMascotAnimationState.Fail));
AddStep("set clear state again", () => mascots.ForEach(mascot => mascot.State.Value = TaikoMascotAnimationState.Clear));
AddAssert("skins with animations change to clear", () => someMascotsIn(TaikoMascotAnimationState.Clear));
}
[Test]
public void TestIdleState()
{
AddStep("set beatmap", () => setBeatmap());
createDrawableRuleset();
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle);
assertStateAfterResult(new JudgementResult(new StrongHitObject(), new TaikoStrongJudgement()) { Type = HitResult.Miss }, TaikoMascotAnimationState.Idle);
}
[Test]
public void TestKiaiState()
{
AddStep("set beatmap", () => setBeatmap(true));
createDrawableRuleset();
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Good }, TaikoMascotAnimationState.Kiai);
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoStrongJudgement()) { Type = HitResult.Miss }, TaikoMascotAnimationState.Kiai);
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Miss }, TaikoMascotAnimationState.Fail);
}
[Test]
public void TestMissState()
{
AddStep("set beatmap", () => setBeatmap());
createDrawableRuleset();
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle);
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Miss }, TaikoMascotAnimationState.Fail);
assertStateAfterResult(new JudgementResult(new DrumRoll(), new TaikoDrumRollJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Fail);
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Good }, TaikoMascotAnimationState.Idle);
}
[TestCase(true)]
[TestCase(false)]
public void TestClearStateOnComboMilestone(bool kiai)
{
AddStep("set beatmap", () => setBeatmap(kiai));
createDrawableRuleset();
AddRepeatStep("reach 49 combo", () => applyNewResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }), 49);
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Good }, TaikoMascotAnimationState.Clear);
}
[TestCase(true, TaikoMascotAnimationState.Kiai)]
[TestCase(false, TaikoMascotAnimationState.Idle)]
public void TestClearStateOnClearedSwell(bool kiai, TaikoMascotAnimationState expectedStateAfterClear)
{
AddStep("set beatmap", () => setBeatmap(kiai));
createDrawableRuleset();
assertStateAfterResult(new JudgementResult(new Swell(), new TaikoSwellJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Clear);
AddUntilStep($"state reverts to {expectedStateAfterClear.ToString().ToLower()}", () => allMascotsIn(expectedStateAfterClear));
}
private void setBeatmap(bool kiai = false)
{
var controlPointInfo = new ControlPointInfo();
controlPointInfo.Add(0, new TimingControlPoint { BeatLength = 90 });
if (kiai)
controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true });
Beatmap.Value = 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 = "Craftplacer",
},
Ruleset = new TaikoRuleset().RulesetInfo
},
ControlPointInfo = controlPointInfo
});
scoreProcessor.ApplyBeatmap(Beatmap.Value.Beatmap);
}
private void createDrawableRuleset()
{
AddUntilStep("wait for beatmap to be loaded", () => Beatmap.Value.Track.IsLoaded);
AddStep("create drawable ruleset", () =>
{
Beatmap.Value.Track.Start();
SetContents(() =>
{
var ruleset = new TaikoRuleset();
return new DrawableTaikoRuleset(ruleset, Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo));
});
});
}
private void assertStateAfterResult(JudgementResult judgementResult, TaikoMascotAnimationState expectedState)
{
AddStep($"{judgementResult.Type.ToString().ToLower()} result for {judgementResult.Judgement.GetType().Name.Humanize(LetterCasing.LowerCase)}",
() => applyNewResult(judgementResult));
AddAssert($"state is {expectedState.ToString().ToLower()}", () => allMascotsIn(expectedState));
}
private void applyNewResult(JudgementResult judgementResult)
{
scoreProcessor.ApplyResult(judgementResult);
foreach (var playfield in playfields)
{
var hit = new DrawableTestHit(new Hit(), judgementResult.Type);
Add(hit);
playfield.OnNewResult(hit, judgementResult);
}
foreach (var mascot in mascots)
{
mascot.LastResult.Value = judgementResult;
}
}
private bool allMascotsIn(TaikoMascotAnimationState state) => mascots.All(d => d.State.Value == state);
private bool someMascotsIn(TaikoMascotAnimationState state) => mascots.Any(d => d.State.Value == state);
}
}

View File

@ -24,6 +24,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
typeof(TaikoHitTarget),
typeof(TaikoLegacyHitTarget),
typeof(PlayfieldBackgroundRight),
typeof(LegacyTaikoScroller),
}).ToList();
[Cached(typeof(IScrollingInfo))]
@ -51,6 +52,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Height = 0.6f,
}));
AddRepeatStep("change height", () => this.ChildrenOfType<TaikoPlayfield>().ForEach(p => p.Height = Math.Max(0.2f, (p.Height + 0.2f) % 1f)), 50);

View File

@ -0,0 +1,22 @@
// 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.Extensions.IEnumerableExtensions;
using osu.Framework.Testing;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Skinning;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.Tests.Skinning
{
public class TestSceneTaikoScroller : TaikoSkinnableTestScene
{
public TestSceneTaikoScroller()
{
AddStep("Load scroller", () => SetContents(() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.Scroller), _ => Empty())));
AddToggleStep("Toggle passing", passing => this.ChildrenOfType<LegacyTaikoScroller>().ForEach(s => s.LastResult.Value =
new JudgementResult(null, new Judgement()) { Type = passing ? HitResult.Perfect : HitResult.Miss }));
}
}
}

View File

@ -167,13 +167,15 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
default:
{
bool isRim = samples.Any(s => s.Name == HitSampleInfo.HIT_CLAP || s.Name == HitSampleInfo.HIT_WHISTLE);
bool isRimDefinition(HitSampleInfo s) => s.Name == HitSampleInfo.HIT_CLAP || s.Name == HitSampleInfo.HIT_WHISTLE;
bool isRim = samples.Any(isRimDefinition);
yield return new Hit
{
StartTime = obj.StartTime,
Type = isRim ? HitType.Rim : HitType.Centre,
Samples = obj.Samples,
Samples = samples,
IsStrong = strong
};

View File

@ -2,9 +2,11 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Graphics;
using osu.Game.Audio;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
@ -47,6 +49,37 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
? new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.CentreHit), _ => new CentreHitCirclePiece(), confineMode: ConfineMode.ScaleToFit)
: new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.RimHit), _ => new RimHitCirclePiece(), confineMode: ConfineMode.ScaleToFit);
protected override IEnumerable<HitSampleInfo> GetSamples()
{
// normal and claps are always handled by the drum (see DrumSampleMapping).
var samples = HitObject.Samples.Where(s => s.Name != HitSampleInfo.HIT_NORMAL && s.Name != HitSampleInfo.HIT_CLAP);
if (HitObject.Type == HitType.Rim && HitObject.IsStrong)
{
// strong + rim always maps to whistle.
// TODO: this should really be in the legacy decoder, but can't be because legacy encoding parity would be broken.
// when we add a taiko editor, this is probably not going to play nice.
var corrected = samples.ToList();
for (var i = 0; i < corrected.Count; i++)
{
var s = corrected[i];
if (s.Name != HitSampleInfo.HIT_FINISH)
continue;
var sClone = s.Clone();
sClone.Name = HitSampleInfo.HIT_WHISTLE;
corrected[i] = sClone;
}
return corrected;
}
return samples;
}
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
Debug.Assert(HitObject.HitWindows != null);

View File

@ -165,8 +165,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
return base.CreateNestedHitObject(hitObject);
}
// Normal and clap samples are handled by the drum
protected override IEnumerable<HitSampleInfo> GetSamples() => HitObject.Samples.Where(s => s.Name != HitSampleInfo.HIT_NORMAL && s.Name != HitSampleInfo.HIT_CLAP);
// Most osu!taiko hitsounds are managed by the drum (see DrumSampleMapping).
protected override IEnumerable<HitSampleInfo> GetSamples() => Enumerable.Empty<HitSampleInfo>();
protected abstract SkinnableDrawable CreateMainPiece();

View File

@ -0,0 +1,149 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Taiko.Skinning
{
public class LegacyTaikoScroller : CompositeDrawable
{
public LegacyTaikoScroller()
{
RelativeSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader(true)]
private void load(GameplayBeatmap gameplayBeatmap)
{
if (gameplayBeatmap != null)
((IBindable<JudgementResult>)LastResult).BindTo(gameplayBeatmap.LastJudgementResult);
}
private bool passing;
protected override void LoadComplete()
{
base.LoadComplete();
LastResult.BindValueChanged(result =>
{
var r = result.NewValue;
// always ignore hitobjects that don't affect combo (drumroll ticks etc.)
if (r?.Judgement.AffectsCombo == false)
return;
passing = r == null || r.Type > HitResult.Miss;
foreach (var sprite in InternalChildren.OfType<ScrollerSprite>())
sprite.Passing = passing;
}, true);
}
public Bindable<JudgementResult> LastResult = new Bindable<JudgementResult>();
protected override void Update()
{
base.Update();
while (true)
{
float? additiveX = null;
foreach (var sprite in InternalChildren)
{
// add the x coordinates and perform re-layout on all sprites as spacing may change with gameplay scale.
sprite.X = additiveX ??= sprite.X - (float)Time.Elapsed * 0.1f;
additiveX += sprite.DrawWidth - 1;
if (sprite.X + sprite.DrawWidth < 0)
sprite.Expire();
}
var last = InternalChildren.LastOrDefault();
// only break from this loop once we have saturated horizontal space completely.
if (last != null && last.ScreenSpaceDrawQuad.TopRight.X >= ScreenSpaceDrawQuad.TopRight.X)
break;
AddInternal(new ScrollerSprite
{
Passing = passing
});
}
}
private class ScrollerSprite : CompositeDrawable
{
private Sprite passingSprite;
private Sprite failingSprite;
private bool passing = true;
public bool Passing
{
get => passing;
set
{
if (value == passing)
return;
passing = value;
if (IsLoaded)
updatePassing();
}
}
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
{
AutoSizeAxes = Axes.X;
RelativeSizeAxes = Axes.Y;
FillMode = FillMode.Fit;
InternalChildren = new Drawable[]
{
passingSprite = new Sprite { Texture = skin.GetTexture("taiko-slider") },
failingSprite = new Sprite { Texture = skin.GetTexture("taiko-slider-fail"), Alpha = 0 },
};
updatePassing();
}
protected override void Update()
{
base.Update();
foreach (var c in InternalChildren)
c.Scale = new Vector2(DrawHeight / c.Height);
}
private void updatePassing()
{
if (passing)
{
passingSprite.Show();
failingSprite.FadeOut(200);
}
else
{
failingSprite.FadeIn(200);
passingSprite.Delay(200).FadeOut();
}
}
}
}
}

View File

@ -8,6 +8,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Textures;
using osu.Game.Audio;
using osu.Game.Rulesets.Taiko.UI;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.Skinning
@ -85,6 +86,18 @@ namespace osu.Game.Rulesets.Taiko.Skinning
return new LegacyHitExplosion(sprite);
return null;
case TaikoSkinComponents.Scroller:
if (GetTexture("taiko-slider") != null)
return new LegacyTaikoScroller();
return null;
case TaikoSkinComponents.Mascot:
if (GetTexture("pippidonclear0") != null)
return new DrawableTaikoMascot();
return null;
}
return source.GetDrawableComponent(component);

View File

@ -18,5 +18,7 @@ namespace osu.Game.Rulesets.Taiko
TaikoExplosionMiss,
TaikoExplosionGood,
TaikoExplosionGreat,
Scroller,
Mascot,
}
}

View File

@ -0,0 +1,123 @@
// 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.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Textures;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Taiko.Judgements;
using osu.Game.Screens.Play;
namespace osu.Game.Rulesets.Taiko.UI
{
public class DrawableTaikoMascot : BeatSyncedContainer
{
public readonly Bindable<TaikoMascotAnimationState> State;
public readonly Bindable<JudgementResult> LastResult;
private readonly Dictionary<TaikoMascotAnimationState, TaikoMascotAnimation> animations;
private TaikoMascotAnimation currentAnimation;
private bool lastObjectHit = true;
private bool kiaiMode;
public DrawableTaikoMascot(TaikoMascotAnimationState startingState = TaikoMascotAnimationState.Idle)
{
Origin = Anchor = Anchor.BottomLeft;
State = new Bindable<TaikoMascotAnimationState>(startingState);
LastResult = new Bindable<JudgementResult>();
animations = new Dictionary<TaikoMascotAnimationState, TaikoMascotAnimation>();
}
[BackgroundDependencyLoader(true)]
private void load(TextureStore textures, GameplayBeatmap gameplayBeatmap)
{
InternalChildren = new[]
{
animations[TaikoMascotAnimationState.Idle] = new TaikoMascotAnimation(TaikoMascotAnimationState.Idle),
animations[TaikoMascotAnimationState.Clear] = new TaikoMascotAnimation(TaikoMascotAnimationState.Clear),
animations[TaikoMascotAnimationState.Kiai] = new TaikoMascotAnimation(TaikoMascotAnimationState.Kiai),
animations[TaikoMascotAnimationState.Fail] = new TaikoMascotAnimation(TaikoMascotAnimationState.Fail),
};
if (gameplayBeatmap != null)
((IBindable<JudgementResult>)LastResult).BindTo(gameplayBeatmap.LastJudgementResult);
}
protected override void LoadComplete()
{
base.LoadComplete();
animations.Values.ForEach(animation => animation.Hide());
State.BindValueChanged(mascotStateChanged, true);
LastResult.BindValueChanged(onNewResult);
}
private void onNewResult(ValueChangedEvent<JudgementResult> resultChangedEvent)
{
var result = resultChangedEvent.NewValue;
if (result == null)
return;
// TODO: missing support for clear/fail state transition at end of beatmap gameplay
if (triggerComboClear(result) || triggerSwellClear(result))
{
State.Value = TaikoMascotAnimationState.Clear;
// always consider a clear equivalent to a hit to avoid clear -> miss transitions
lastObjectHit = true;
}
if (!result.Judgement.AffectsCombo)
return;
lastObjectHit = result.IsHit;
}
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes)
{
kiaiMode = effectPoint.KiaiMode;
}
protected override void Update()
{
base.Update();
State.Value = getNextState();
}
private TaikoMascotAnimationState getNextState()
{
// don't change state if current animation is still playing (and we haven't rewound before it).
// used for clear state - others are manually animated on new beats.
if (currentAnimation?.Completed == false && currentAnimation.DisplayTime <= Time.Current)
return State.Value;
if (!lastObjectHit)
return TaikoMascotAnimationState.Fail;
return kiaiMode ? TaikoMascotAnimationState.Kiai : TaikoMascotAnimationState.Idle;
}
private void mascotStateChanged(ValueChangedEvent<TaikoMascotAnimationState> state)
{
currentAnimation?.Hide();
currentAnimation = animations[state.NewValue];
currentAnimation.Show();
}
private bool triggerComboClear(JudgementResult judgementResult)
=> (judgementResult.ComboAtJudgement + 1) % 50 == 0 && judgementResult.Judgement.AffectsCombo && judgementResult.IsHit;
private bool triggerSwellClear(JudgementResult judgementResult)
=> judgementResult.Judgement is TaikoSwellJudgement && judgementResult.IsHit;
}
}

View File

@ -3,6 +3,7 @@
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Objects;
@ -16,11 +17,15 @@ using osu.Game.Replays;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Taiko.UI
{
public class DrawableTaikoRuleset : DrawableScrollingRuleset<TaikoHitObject>
{
private SkinnableDrawable scroller;
protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Overlapping;
protected override bool UserScrollSpeedAdjustment => false;
@ -36,6 +41,20 @@ namespace osu.Game.Rulesets.Taiko.UI
private void load()
{
new BarLineGenerator<BarLine>(Beatmap).BarLines.ForEach(bar => Playfield.Add(bar.Major ? new DrawableBarLineMajor(bar) : new DrawableBarLine(bar)));
AddInternal(scroller = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.Scroller), _ => Empty())
{
RelativeSizeAxes = Axes.X,
Depth = float.MaxValue
});
}
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
var playfieldScreen = Playfield.ScreenSpaceDrawQuad;
scroller.Height = ToLocalSpace(playfieldScreen.TopLeft + new Vector2(0, playfieldScreen.Height / 20)).Y;
}
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new TaikoPlayfieldAdjustmentContainer();

View File

@ -0,0 +1,133 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Textures;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Containers;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Taiko.UI
{
public sealed class TaikoMascotAnimation : BeatSyncedContainer
{
private readonly TextureAnimation textureAnimation;
private int currentFrame;
public double DisplayTime;
public TaikoMascotAnimation(TaikoMascotAnimationState state)
{
InternalChild = textureAnimation = createTextureAnimation(state).With(animation =>
{
animation.Origin = animation.Anchor = Anchor.BottomLeft;
animation.Scale = new Vector2(0.51f); // close enough to stable
});
RelativeSizeAxes = Axes.Both;
Origin = Anchor = Anchor.BottomLeft;
// needs to be always present to prevent the animation clock consuming time spent when not present.
AlwaysPresent = true;
}
public bool Completed => !textureAnimation.IsPlaying || textureAnimation.PlaybackPosition >= textureAnimation.Duration;
public override void Show()
{
base.Show();
DisplayTime = Time.Current;
textureAnimation.Seek(0);
}
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes)
{
// assume that if the animation is playing on its own, it's independent from the beat and doesn't need to be touched.
if (textureAnimation.FrameCount == 0 || textureAnimation.IsPlaying)
return;
textureAnimation.GotoFrame(currentFrame);
currentFrame = (currentFrame + 1) % textureAnimation.FrameCount;
}
private static TextureAnimation createTextureAnimation(TaikoMascotAnimationState state)
{
switch (state)
{
case TaikoMascotAnimationState.Clear:
return new ClearMascotTextureAnimation();
case TaikoMascotAnimationState.Idle:
case TaikoMascotAnimationState.Kiai:
case TaikoMascotAnimationState.Fail:
return new ManualMascotTextureAnimation(state);
default:
throw new ArgumentOutOfRangeException(nameof(state), $"Mascot animations for state {state} are not supported");
}
}
private class ManualMascotTextureAnimation : TextureAnimation
{
private readonly TaikoMascotAnimationState state;
public ManualMascotTextureAnimation(TaikoMascotAnimationState state)
{
this.state = state;
IsPlaying = false;
}
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
{
for (int frameIndex = 0; true; frameIndex++)
{
var texture = getAnimationFrame(skin, state, frameIndex);
if (texture == null)
break;
AddFrame(texture);
}
}
}
private class ClearMascotTextureAnimation : TextureAnimation
{
private const float clear_animation_speed = 1000 / 10f;
private static readonly int[] clear_animation_sequence = { 0, 1, 2, 3, 4, 5, 6, 5, 6, 5, 4, 3, 2, 1, 0 };
public ClearMascotTextureAnimation()
{
DefaultFrameLength = clear_animation_speed;
Loop = false;
}
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
{
foreach (var frameIndex in clear_animation_sequence)
{
var texture = getAnimationFrame(skin, TaikoMascotAnimationState.Clear, frameIndex);
if (texture == null)
// as per https://osu.ppy.sh/help/wiki/Skinning/osu!taiko#pippidon
break;
AddFrame(texture);
}
}
}
private static Texture getAnimationFrame(ISkin skin, TaikoMascotAnimationState state, int frameIndex)
=> skin.GetTexture($"pippidon{state.ToString().ToLower()}{frameIndex}");
}
}

View File

@ -0,0 +1,13 @@
// 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.
namespace osu.Game.Rulesets.Taiko.UI
{
public enum TaikoMascotAnimationState
{
Idle,
Clear,
Kiai,
Fail
}
}

View File

@ -15,6 +15,7 @@ using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Judgements;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Taiko.UI
{
@ -32,6 +33,7 @@ namespace osu.Game.Rulesets.Taiko.UI
private JudgementContainer<DrawableTaikoJudgement> judgementContainer;
private ScrollingHitObjectContainer drumRollHitContainer;
internal Drawable HitTarget;
private SkinnableDrawable mascot;
private ProxyContainer topLevelHitContainer;
private ProxyContainer barlineContainer;
@ -125,12 +127,20 @@ namespace osu.Game.Rulesets.Taiko.UI
},
}
},
mascot = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.Mascot), _ => Empty())
{
Origin = Anchor.BottomLeft,
Anchor = Anchor.TopLeft,
RelativePositionAxes = Axes.Y,
RelativeSizeAxes = Axes.None,
Y = 0.2f
},
topLevelHitContainer = new ProxyContainer
{
Name = "Top level hit objects",
RelativeSizeAxes = Axes.Both,
},
drumRollHitContainer.CreateProxy()
drumRollHitContainer.CreateProxy(),
};
}
@ -142,6 +152,8 @@ namespace osu.Game.Rulesets.Taiko.UI
// This is basically allowing for correct alignment as relative pieces move around them.
rightArea.Padding = new MarginPadding { Left = leftArea.DrawWidth };
hitTargetOffsetContent.Padding = new MarginPadding { Left = HitTarget.DrawWidth / 2 };
mascot.Scale = new Vector2(DrawHeight / DEFAULT_HEIGHT);
}
public override void Add(DrawableHitObject h)

View File

@ -6,6 +6,7 @@ using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using NUnit.Framework;
using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Textures;
@ -28,14 +29,15 @@ namespace osu.Game.Tests.Beatmaps.Formats
private static IEnumerable<string> allBeatmaps => TestResources.GetStore().GetAvailableResources().Where(res => res.EndsWith(".osu"));
[TestCaseSource(nameof(allBeatmaps))]
public void TestBeatmap(string name)
public void TestEncodeDecodeStability(string name)
{
var decoded = decode(name, out var encoded);
var decoded = decodeFromLegacy(TestResources.GetStore().GetStream(name));
var decodedAfterEncode = decodeFromLegacy(encodeToLegacy(decoded));
sort(decoded);
sort(encoded);
sort(decodedAfterEncode);
Assert.That(encoded.Serialize(), Is.EqualTo(decoded.Serialize()));
Assert.That(decodedAfterEncode.Serialize(), Is.EqualTo(decoded.Serialize()));
}
private void sort(IBeatmap beatmap)
@ -48,27 +50,22 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
private IBeatmap decode(string filename, out IBeatmap encoded)
private IBeatmap decodeFromLegacy(Stream stream)
{
using (var stream = TestResources.GetStore().GetStream(filename))
using (var sr = new LineBufferedReader(stream))
{
var legacyDecoded = convert(new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(sr));
using (var reader = new LineBufferedReader(stream))
return convert(new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(reader));
}
using (var ms = new MemoryStream())
using (var sw = new StreamWriter(ms))
using (var sr2 = new LineBufferedReader(ms, true))
{
new LegacyBeatmapEncoder(legacyDecoded).Encode(sw);
private Stream encodeToLegacy(IBeatmap beatmap)
{
var stream = new MemoryStream();
sw.Flush();
ms.Position = 0;
using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true))
new LegacyBeatmapEncoder(beatmap).Encode(writer);
encoded = convert(new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(sr2));
stream.Position = 0;
return legacyDecoded;
}
}
return stream;
}
private IBeatmap convert(IBeatmap beatmap)

View File

@ -0,0 +1,241 @@
// 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 System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Platform;
using osu.Game.Configuration;
using osu.Game.IO;
namespace osu.Game.Tests.NonVisual
{
[TestFixture]
public class CustomDataDirectoryTest
{
[SetUp]
public void SetUp()
{
if (Directory.Exists(customPath))
Directory.Delete(customPath, true);
}
[Test]
public void TestDefaultDirectory()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestDefaultDirectory)))
{
try
{
var osu = loadOsu(host);
var storage = osu.Dependencies.Get<Storage>();
string defaultStorageLocation = Path.Combine(Environment.CurrentDirectory, "headless", nameof(TestDefaultDirectory));
Assert.That(storage.GetFullPath("."), Is.EqualTo(defaultStorageLocation));
}
finally
{
host.Exit();
}
}
}
private string customPath => Path.Combine(Environment.CurrentDirectory, "custom-path");
[Test]
public void TestCustomDirectory()
{
using (var host = new HeadlessGameHost(nameof(TestCustomDirectory)))
{
string headlessPrefix = Path.Combine("headless", nameof(TestCustomDirectory));
// need access before the game has constructed its own storage yet.
Storage storage = new DesktopStorage(headlessPrefix, host);
// manual cleaning so we can prepare a config file.
storage.DeleteDirectory(string.Empty);
using (var storageConfig = new StorageConfigManager(storage))
storageConfig.Set(StorageConfig.FullPath, customPath);
try
{
var osu = loadOsu(host);
// switch to DI'd storage
storage = osu.Dependencies.Get<Storage>();
Assert.That(storage.GetFullPath("."), Is.EqualTo(customPath));
}
finally
{
host.Exit();
}
}
}
[Test]
public void TestSubDirectoryLookup()
{
using (var host = new HeadlessGameHost(nameof(TestSubDirectoryLookup)))
{
string headlessPrefix = Path.Combine("headless", nameof(TestSubDirectoryLookup));
// need access before the game has constructed its own storage yet.
Storage storage = new DesktopStorage(headlessPrefix, host);
// manual cleaning so we can prepare a config file.
storage.DeleteDirectory(string.Empty);
using (var storageConfig = new StorageConfigManager(storage))
storageConfig.Set(StorageConfig.FullPath, customPath);
try
{
var osu = loadOsu(host);
// switch to DI'd storage
storage = osu.Dependencies.Get<Storage>();
string actualTestFile = Path.Combine(customPath, "rulesets", "test");
File.WriteAllText(actualTestFile, "test");
var rulesetStorage = storage.GetStorageForDirectory("rulesets");
var lookupPath = rulesetStorage.GetFiles(".").Single();
Assert.That(lookupPath, Is.EqualTo("test"));
}
finally
{
host.Exit();
}
}
}
[Test]
public void TestMigration()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestMigration)))
{
try
{
var osu = loadOsu(host);
var storage = osu.Dependencies.Get<Storage>();
// ensure we perform a save
host.Dependencies.Get<FrameworkConfigManager>().Save();
// ensure we "use" cache
host.Storage.GetStorageForDirectory("cache");
// for testing nested files are not ignored (only top level)
host.Storage.GetStorageForDirectory("test-nested").GetStorageForDirectory("cache");
string defaultStorageLocation = Path.Combine(Environment.CurrentDirectory, "headless", nameof(TestMigration));
Assert.That(storage.GetFullPath("."), Is.EqualTo(defaultStorageLocation));
osu.Migrate(customPath);
Assert.That(storage.GetFullPath("."), Is.EqualTo(customPath));
// ensure cache was not moved
Assert.That(host.Storage.ExistsDirectory("cache"));
// ensure nested cache was moved
Assert.That(!host.Storage.ExistsDirectory(Path.Combine("test-nested", "cache")));
Assert.That(storage.ExistsDirectory(Path.Combine("test-nested", "cache")));
foreach (var file in OsuStorage.IGNORE_FILES)
{
Assert.That(host.Storage.Exists(file), Is.True);
Assert.That(storage.Exists(file), Is.False);
}
foreach (var dir in OsuStorage.IGNORE_DIRECTORIES)
{
Assert.That(host.Storage.ExistsDirectory(dir), Is.True);
Assert.That(storage.ExistsDirectory(dir), Is.False);
}
Assert.That(new StreamReader(host.Storage.GetStream("storage.ini")).ReadToEnd().Contains($"FullPath = {customPath}"));
}
finally
{
host.Exit();
}
}
}
[Test]
public void TestMigrationBetweenTwoTargets()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestMigrationBetweenTwoTargets)))
{
try
{
var osu = loadOsu(host);
string customPath2 = $"{customPath}-2";
const string database_filename = "client.db";
Assert.DoesNotThrow(() => osu.Migrate(customPath));
Assert.That(File.Exists(Path.Combine(customPath, database_filename)));
Assert.DoesNotThrow(() => osu.Migrate(customPath2));
Assert.That(File.Exists(Path.Combine(customPath2, database_filename)));
Assert.DoesNotThrow(() => osu.Migrate(customPath));
Assert.That(File.Exists(Path.Combine(customPath, database_filename)));
}
finally
{
host.Exit();
}
}
}
[Test]
public void TestMigrationToSameTargetFails()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestMigrationToSameTargetFails)))
{
try
{
var osu = loadOsu(host);
Assert.DoesNotThrow(() => osu.Migrate(customPath));
Assert.Throws<InvalidOperationException>(() => osu.Migrate(customPath));
}
finally
{
host.Exit();
}
}
}
private OsuGameBase loadOsu(GameHost host)
{
var osu = new OsuGameBase();
Task.Run(() => host.Run(osu));
waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time");
return osu;
}
private static void waitForOrAssert(Func<bool> result, string failureMessage, int timeout = 60000)
{
Task task = Task.Run(() =>
{
while (!result()) Thread.Sleep(200);
});
Assert.IsTrue(task.Wait(timeout), failureMessage);
}
}
}

View File

@ -152,11 +152,12 @@ namespace osu.Game.Tests.Skins
}
[Test]
public void TestSetBeatmapVersionNoFallback()
public void TestSetBeatmapVersionFallsBackToUserSkin()
{
// completely ignoring beatmap versions for simplicity.
AddStep("Set user skin version 2.3", () => userSource.Configuration.LegacyVersion = 2.3m);
AddStep("Set beatmap skin version null", () => beatmapSource.Configuration.LegacyVersion = 1.7m);
AddAssert("Check legacy version lookup", () => requester.GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version)?.Value == 1.7m);
AddAssert("Check legacy version lookup", () => requester.GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version)?.Value == 2.3m);
}
[Test]
@ -172,7 +173,6 @@ namespace osu.Game.Tests.Skins
public void TestIniWithNoVersionFallsBackTo1()
{
AddStep("Parse skin with no version", () => userSource.Configuration = new LegacySkinDecoder().Decode(new LineBufferedReader(new MemoryStream())));
AddStep("Set beatmap skin version null", () => beatmapSource.Configuration.LegacyVersion = null);
AddAssert("Check legacy version lookup", () => requester.GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version)?.Value == 1.0m);
}

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@ -84,7 +85,7 @@ namespace osu.Game.Beatmaps
public IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList<Mod> mods = null, TimeSpan? timeout = null)
{
using (var cancellationSource = new CancellationTokenSource(timeout ?? TimeSpan.FromSeconds(10)))
using (var cancellationSource = createCancellationTokenSource(timeout))
{
mods ??= Array.Empty<Mod>();
@ -181,6 +182,15 @@ namespace osu.Game.Beatmaps
beatmapLoadTask = null;
}
private CancellationTokenSource createCancellationTokenSource(TimeSpan? timeout)
{
if (Debugger.IsAttached)
// ignore timeout when debugger is attached (may be breakpointing / debugging).
return new CancellationTokenSource();
return new CancellationTokenSource(timeout ?? TimeSpan.FromSeconds(10));
}
private Task<IBeatmap> loadBeatmapAsync() => beatmapLoadTask ??= Task.Factory.StartNew(() =>
{
// Todo: Handle cancellation during beatmap parsing

View File

@ -0,0 +1,30 @@
// 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.Configuration;
using osu.Framework.Platform;
namespace osu.Game.Configuration
{
public class StorageConfigManager : IniConfigManager<StorageConfig>
{
protected override string Filename => "storage.ini";
public StorageConfigManager(Storage storage)
: base(storage)
{
}
protected override void InitialiseDefaults()
{
base.InitialiseDefaults();
Set(StorageConfig.FullPath, string.Empty);
}
}
public enum StorageConfig
{
FullPath,
}
}

View File

@ -160,5 +160,13 @@ namespace osu.Game.Database
}
}
}
public void FlushConnections()
{
foreach (var context in threadContexts.Values)
context.Dispose();
recycleThreadContexts();
}
}
}

132
osu.Game/IO/OsuStorage.cs Normal file
View File

@ -0,0 +1,132 @@
// 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 System.Threading;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Configuration;
namespace osu.Game.IO
{
public class OsuStorage : WrappedStorage
{
private readonly GameHost host;
private readonly StorageConfigManager storageConfig;
internal static readonly string[] IGNORE_DIRECTORIES = { "cache" };
internal static readonly string[] IGNORE_FILES =
{
"framework.ini",
"storage.ini"
};
public OsuStorage(GameHost host)
: base(host.Storage, string.Empty)
{
this.host = host;
storageConfig = new StorageConfigManager(host.Storage);
var customStoragePath = storageConfig.Get<string>(StorageConfig.FullPath);
if (!string.IsNullOrEmpty(customStoragePath))
ChangeTargetStorage(host.GetStorage(customStoragePath));
}
protected override void ChangeTargetStorage(Storage newStorage)
{
base.ChangeTargetStorage(newStorage);
Logger.Storage = UnderlyingStorage.GetStorageForDirectory("logs");
}
public void Migrate(string newLocation)
{
var source = new DirectoryInfo(GetFullPath("."));
var destination = new DirectoryInfo(newLocation);
// ensure the new location has no files present, else hard abort
if (destination.Exists)
{
if (destination.GetFiles().Length > 0 || destination.GetDirectories().Length > 0)
throw new InvalidOperationException("Migration destination already has files present");
deleteRecursive(destination);
}
copyRecursive(source, destination);
ChangeTargetStorage(host.GetStorage(newLocation));
storageConfig.Set(StorageConfig.FullPath, newLocation);
storageConfig.Save();
deleteRecursive(source);
}
private static void deleteRecursive(DirectoryInfo target, bool topLevelExcludes = true)
{
foreach (System.IO.FileInfo fi in target.GetFiles())
{
if (topLevelExcludes && IGNORE_FILES.Contains(fi.Name))
continue;
fi.Delete();
}
foreach (DirectoryInfo dir in target.GetDirectories())
{
if (topLevelExcludes && IGNORE_DIRECTORIES.Contains(dir.Name))
continue;
dir.Delete(true);
}
}
private static void copyRecursive(DirectoryInfo source, DirectoryInfo destination, bool topLevelExcludes = true)
{
// based off example code https://docs.microsoft.com/en-us/dotnet/api/system.io.directoryinfo
Directory.CreateDirectory(destination.FullName);
foreach (System.IO.FileInfo fi in source.GetFiles())
{
if (topLevelExcludes && IGNORE_FILES.Contains(fi.Name))
continue;
attemptCopy(fi, Path.Combine(destination.FullName, fi.Name));
}
foreach (DirectoryInfo dir in source.GetDirectories())
{
if (topLevelExcludes && IGNORE_DIRECTORIES.Contains(dir.Name))
continue;
copyRecursive(dir, destination.CreateSubdirectory(dir.Name), false);
}
}
private static void attemptCopy(System.IO.FileInfo fileInfo, string destination)
{
int tries = 5;
while (true)
{
try
{
fileInfo.CopyTo(destination, true);
return;
}
catch (Exception)
{
if (tries-- == 0)
throw;
}
Thread.Sleep(50);
}
}
}
}

View File

@ -9,12 +9,12 @@ using Newtonsoft.Json.Linq;
namespace osu.Game.IO.Serialization.Converters
{
/// <summary>
/// A type of <see cref="JsonConverter"/> that serializes a <see cref="List{T}"/> alongside
/// A type of <see cref="JsonConverter"/> that serializes an <see cref="IReadOnlyList{T}"/> alongside
/// a lookup table for the types contained. The lookup table is used in deserialization to
/// reconstruct the objects with their original types.
/// </summary>
/// <typeparam name="T">The type of objects contained in the <see cref="List{T}"/> this attribute is attached to.</typeparam>
public class TypedListConverter<T> : JsonConverter
/// <typeparam name="T">The type of objects contained in the <see cref="IReadOnlyList{T}"/> this attribute is attached to.</typeparam>
public class TypedListConverter<T> : JsonConverter<IReadOnlyList<T>>
{
private readonly bool requiresTypeVersion;
@ -36,9 +36,7 @@ namespace osu.Game.IO.Serialization.Converters
this.requiresTypeVersion = requiresTypeVersion;
}
public override bool CanConvert(Type objectType) => objectType == typeof(List<T>);
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
public override IReadOnlyList<T> ReadJson(JsonReader reader, Type objectType, IReadOnlyList<T> existingValue, bool hasExistingValue, JsonSerializer serializer)
{
var list = new List<T>();
@ -59,14 +57,12 @@ namespace osu.Game.IO.Serialization.Converters
return list;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
public override void WriteJson(JsonWriter writer, IReadOnlyList<T> value, JsonSerializer serializer)
{
var list = (IEnumerable<T>)value;
var lookupTable = new List<string>();
var objects = new List<JObject>();
foreach (var item in list)
foreach (var item in value)
{
var type = item.GetType();
var assemblyName = type.Assembly.GetName();

View File

@ -11,26 +11,22 @@ namespace osu.Game.IO.Serialization.Converters
/// <summary>
/// A type of <see cref="JsonConverter"/> that serializes only the X and Y coordinates of a <see cref="Vector2"/>.
/// </summary>
public class Vector2Converter : JsonConverter
public class Vector2Converter : JsonConverter<Vector2>
{
public override bool CanConvert(Type objectType) => objectType == typeof(Vector2);
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
public override Vector2 ReadJson(JsonReader reader, Type objectType, Vector2 existingValue, bool hasExistingValue, JsonSerializer serializer)
{
var obj = JObject.Load(reader);
return new Vector2((float)obj["x"], (float)obj["y"]);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
public override void WriteJson(JsonWriter writer, Vector2 value, JsonSerializer serializer)
{
var vector = (Vector2)value;
writer.WriteStartObject();
writer.WritePropertyName("x");
writer.WriteValue(vector.X);
writer.WriteValue(value.X);
writer.WritePropertyName("y");
writer.WriteValue(vector.Y);
writer.WriteValue(value.Y);
writer.WriteEndObject();
}

View File

@ -0,0 +1,88 @@
// 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.Collections.Generic;
using System.IO;
using osu.Framework.Platform;
namespace osu.Game.IO
{
/// <summary>
/// A storage which wraps another storage and delegates implementation, potentially mutating the lookup path.
/// </summary>
public class WrappedStorage : Storage
{
protected Storage UnderlyingStorage { get; private set; }
private readonly string subPath;
public WrappedStorage(Storage underlyingStorage, string subPath = null)
: base(string.Empty)
{
ChangeTargetStorage(underlyingStorage);
this.subPath = subPath;
}
protected virtual string MutatePath(string path) => !string.IsNullOrEmpty(subPath) ? Path.Combine(subPath, path) : path;
protected virtual void ChangeTargetStorage(Storage newStorage)
{
UnderlyingStorage = newStorage;
}
public override string GetFullPath(string path, bool createIfNotExisting = false) =>
UnderlyingStorage.GetFullPath(MutatePath(path), createIfNotExisting);
public override bool Exists(string path) =>
UnderlyingStorage.Exists(MutatePath(path));
public override bool ExistsDirectory(string path) =>
UnderlyingStorage.ExistsDirectory(MutatePath(path));
public override void DeleteDirectory(string path) =>
UnderlyingStorage.DeleteDirectory(MutatePath(path));
public override void Delete(string path) =>
UnderlyingStorage.Delete(MutatePath(path));
public override IEnumerable<string> GetDirectories(string path) =>
ToLocalRelative(UnderlyingStorage.GetDirectories(MutatePath(path)));
public IEnumerable<string> ToLocalRelative(IEnumerable<string> paths)
{
string localRoot = GetFullPath(string.Empty);
foreach (var path in paths)
yield return Path.GetRelativePath(localRoot, UnderlyingStorage.GetFullPath(path));
}
public override IEnumerable<string> GetFiles(string path, string pattern = "*") =>
ToLocalRelative(UnderlyingStorage.GetFiles(MutatePath(path), pattern));
public override Stream GetStream(string path, FileAccess access = FileAccess.Read, FileMode mode = FileMode.OpenOrCreate) =>
UnderlyingStorage.GetStream(MutatePath(path), access, mode);
public override string GetDatabaseConnectionString(string name) =>
UnderlyingStorage.GetDatabaseConnectionString(MutatePath(name));
public override void DeleteDatabase(string name) => UnderlyingStorage.DeleteDatabase(MutatePath(name));
public override void OpenInNativeExplorer() => UnderlyingStorage.OpenInNativeExplorer();
public override Storage GetStorageForDirectory(string path)
{
if (string.IsNullOrEmpty(path))
throw new ArgumentException("Must be non-null and not empty string", nameof(path));
if (!path.EndsWith(Path.DirectorySeparatorChar))
path += Path.DirectorySeparatorChar;
// create non-existing path.
GetFullPath(path, true);
return new WrappedStorage(this, path);
}
}
}

View File

@ -91,7 +91,7 @@ namespace osu.Game
protected BackButton BackButton;
protected SettingsPanel Settings;
protected SettingsOverlay Settings;
private VolumeOverlay volume;
private OsuLogo osuLogo;
@ -767,13 +767,20 @@ namespace osu.Game
private Task asyncLoadStream;
private T loadComponentSingleFile<T>(T d, Action<T> add, bool cache = false)
/// <summary>
/// Queues loading the provided component in sequential fashion.
/// This operation is limited to a single thread to avoid saturating all cores.
/// </summary>
/// <param name="component">The component to load.</param>
/// <param name="loadCompleteAction">An action to invoke on load completion (generally to add the component to the hierarchy).</param>
/// <param name="cache">Whether to cache the component as type <typeparamref name="T"/> into the game dependencies before any scheduling.</param>
private T loadComponentSingleFile<T>(T component, Action<T> loadCompleteAction, bool cache = false)
where T : Drawable
{
if (cache)
dependencies.Cache(d);
dependencies.CacheAs(component);
if (d is OverlayContainer overlay)
if (component is OverlayContainer overlay)
overlays.Add(overlay);
// schedule is here to ensure that all component loads are done after LoadComplete is run (and thus all dependencies are cached).
@ -791,12 +798,12 @@ namespace osu.Game
try
{
Logger.Log($"Loading {d}...", level: LogLevel.Debug);
Logger.Log($"Loading {component}...", level: LogLevel.Debug);
// Since this is running in a separate thread, it is possible for OsuGame to be disposed after LoadComponentAsync has been called
// throwing an exception. To avoid this, the call is scheduled on the update thread, which does not run if IsDisposed = true
Task task = null;
var del = new ScheduledDelegate(() => task = LoadComponentAsync(d, add));
var del = new ScheduledDelegate(() => task = LoadComponentAsync(component, loadCompleteAction));
Scheduler.Add(del);
// The delegate won't complete if OsuGame has been disposed in the meantime
@ -811,7 +818,7 @@ namespace osu.Game
await task;
Logger.Log($"Loaded {d}!", level: LogLevel.Debug);
Logger.Log($"Loaded {component}!", level: LogLevel.Debug);
}
catch (OperationCanceledException)
{
@ -819,7 +826,7 @@ namespace osu.Game
});
});
return d;
return component;
}
protected override bool OnScroll(ScrollEvent e)

View File

@ -132,6 +132,8 @@ namespace osu.Game
dependencies.Cache(contextFactory = new DatabaseContextFactory(Storage));
dependencies.CacheAs(Storage);
var largeStore = new LargeTextureStore(Host.CreateTextureLoaderStore(new NamespacedResourceStore<byte[]>(Resources, @"Textures")));
largeStore.AddStore(Host.CreateTextureLoaderStore(new OnlineStore()));
dependencies.Cache(largeStore);
@ -300,8 +302,8 @@ namespace osu.Game
{
base.SetHost(host);
if (Storage == null)
Storage = host.Storage;
if (Storage == null) // may be non-null for certain tests
Storage = new OsuStorage(host);
if (LocalConfig == null)
LocalConfig = new OsuConfigManager(Storage);
@ -326,6 +328,8 @@ namespace osu.Game
{
base.Dispose(isDisposing);
RulesetStore?.Dispose();
contextFactory.FlushConnections();
}
private class OsuUserInputManager : UserInputManager
@ -353,5 +357,11 @@ namespace osu.Game
public override bool ChangeFocusOnClick => false;
}
}
public void Migrate(string path)
{
contextFactory.FlushConnections();
(Storage as OsuStorage)?.Migrate(path);
}
}
}

View File

@ -250,6 +250,28 @@ namespace osu.Game.Overlays.KeyBinding
finalise();
}
protected override bool OnMidiDown(MidiDownEvent e)
{
if (!HasFocus)
return false;
bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState));
finalise();
return true;
}
protected override void OnMidiUp(MidiUpEvent e)
{
if (!HasFocus)
{
base.OnMidiUp(e);
return;
}
finalise();
}
private void clear()
{
bindTarget.UpdateKeyCombination(InputKey.None);

View File

@ -2,10 +2,12 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Screens.Play
@ -38,5 +40,11 @@ namespace osu.Game.Screens.Play
public IEnumerable<BeatmapStatistic> GetStatistics() => PlayableBeatmap.GetStatistics();
public IBeatmap Clone() => PlayableBeatmap.Clone();
private readonly Bindable<JudgementResult> lastJudgementResult = new Bindable<JudgementResult>();
public IBindable<JudgementResult> LastJudgementResult => lastJudgementResult;
public void ApplyResult(JudgementResult result) => lastJudgementResult.Value = result;
}
}

View File

@ -200,6 +200,7 @@ namespace osu.Game.Screens.Play
{
HealthProcessor.ApplyResult(r);
ScoreProcessor.ApplyResult(r);
gameplayBeatmap.ApplyResult(r);
};
DrawableRuleset.OnRevertResult += r =>

View File

@ -86,7 +86,7 @@ namespace osu.Game.Screens.Select
{
base.Dispose(isDisposing);
api.Unregister(this);
api?.Unregister(this);
}
}
}

View File

@ -27,9 +27,11 @@ namespace osu.Game.Skinning
switch (lookup)
{
case LegacySkinConfiguration.LegacySetting s when s == LegacySkinConfiguration.LegacySetting.Version:
if (Configuration.LegacyVersion is decimal version)
return SkinUtils.As<TValue>(new Bindable<decimal>(version));
// For lookup simplicity, ignore beatmap-level versioning completely.
// If it is decided that we need this due to beatmaps somehow using it, the default (1.0 specified in LegacySkinDecoder.CreateTemplateObject)
// needs to be removed else it will cause incorrect skin behaviours. This is due to the config lookup having no context of which skin
// it should be returning the version for.
return null;
}

View File

@ -24,7 +24,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2020.508.1" />
<PackageReference Include="ppy.osu.Framework" Version="2020.511.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.427.0" />
<PackageReference Include="Sentry" Version="2.1.1" />
<PackageReference Include="SharpCompress" Version="0.25.0" />

View File

@ -70,7 +70,7 @@
<Reference Include="System.Net.Http" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.508.1" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.511.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.427.0" />
</ItemGroup>
<!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. -->
@ -80,7 +80,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2020.508.1" />
<PackageReference Include="ppy.osu.Framework" Version="2020.511.0" />
<PackageReference Include="SharpCompress" Version="0.25.0" />
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="SharpRaven" Version="2.4.0" />