Merge remote-tracking branch 'origin/master' into sorcerer-diffcalc-changes

This commit is contained in:
smoogipoo
2019-03-12 16:21:34 +09:00
73 changed files with 1117 additions and 656 deletions

View File

@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Catch.Tests
{ {
protected override string ResourceAssembly => "osu.Game.Rulesets.Catch"; protected override string ResourceAssembly => "osu.Game.Rulesets.Catch";
[TestCase(4.2038001515546597d, "diffcalc-test")] [TestCase(4.2058561036909863d, "diffcalc-test")]
public void Test(double expected, string name) public void Test(double expected, string name)
=> base.Test(expected, name); => base.Test(expected, name);

View File

@ -10,6 +10,7 @@ using osu.Game.Rulesets.UI;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Replays; using osu.Game.Rulesets.Catch.Replays;
using osu.Game.Rulesets.Replays.Types; using osu.Game.Rulesets.Replays.Types;
using osu.Game.Beatmaps.Legacy; using osu.Game.Beatmaps.Legacy;
@ -99,6 +100,11 @@ namespace osu.Game.Rulesets.Catch
new MultiMod(new CatchModAutoplay(), new ModCinema()), new MultiMod(new CatchModAutoplay(), new ModCinema()),
new CatchModRelax(), new CatchModRelax(),
}; };
case ModType.Fun:
return new Mod[]
{
new MultiMod(new ModWindUp<CatchHitObject>(), new ModWindDown<CatchHitObject>())
};
default: default:
return new Mod[] { }; return new Mod[] { };
} }

View File

@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{ {
public class CatchModAutoplay : ModAutoplay<CatchHitObject> public class CatchModAutoplay : ModAutoplay<CatchHitObject>
{ {
protected override Score CreateReplayScore(Beatmap<CatchHitObject> beatmap) => new Score public override Score CreateReplayScore(IBeatmap beatmap) => new Score
{ {
ScoreInfo = new ScoreInfo { User = new User { Username = "osu!salad!" } }, ScoreInfo = new ScoreInfo { User = new User { Username = "osu!salad!" } },
Replay = new CatchAutoGenerator(beatmap).Generate(), Replay = new CatchAutoGenerator(beatmap).Generate(),

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Audio; using osu.Game.Audio;
@ -25,6 +24,11 @@ namespace osu.Game.Rulesets.Catch.Objects
public double Velocity; public double Velocity;
public double TickDistance; public double TickDistance;
/// <summary>
/// The length of one span of this <see cref="JuiceStream"/>.
/// </summary>
public double SpanDuration => Duration / this.SpanCount();
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
{ {
base.ApplyDefaultsToSelf(controlPointInfo, difficulty); base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
@ -41,19 +45,6 @@ namespace osu.Game.Rulesets.Catch.Objects
protected override void CreateNestedHitObjects() protected override void CreateNestedHitObjects()
{ {
base.CreateNestedHitObjects(); base.CreateNestedHitObjects();
createTicks();
}
private void createTicks()
{
if (TickDistance == 0)
return;
var length = Path.Distance;
var tickDistance = Math.Min(TickDistance, length);
var spanDuration = length / Velocity;
var minDistanceFromEnd = Velocity * 0.01;
var tickSamples = Samples.Select(s => new SampleInfo var tickSamples = Samples.Select(s => new SampleInfo
{ {
@ -62,81 +53,59 @@ namespace osu.Game.Rulesets.Catch.Objects
Volume = s.Volume Volume = s.Volume
}).ToList(); }).ToList();
AddNested(new Fruit SliderEventDescriptor? lastEvent = null;
foreach (var e in SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset))
{ {
Samples = Samples, // generate tiny droplets since the last point
StartTime = StartTime, if (lastEvent != null)
X = X
});
double lastTickTime = StartTime;
for (int span = 0; span < this.SpanCount(); span++)
{
var spanStartTime = StartTime + span * spanDuration;
var reversed = span % 2 == 1;
for (double d = tickDistance;; d += tickDistance)
{ {
bool isLastTick = false; double sinceLastTick = e.Time - lastEvent.Value.Time;
if (d + minDistanceFromEnd >= length)
if (sinceLastTick > 80)
{ {
d = length; double timeBetweenTiny = sinceLastTick;
isLastTick = true; while (timeBetweenTiny > 100)
} timeBetweenTiny /= 2;
var timeProgress = d / length; for (double t = timeBetweenTiny; t < sinceLastTick; t += timeBetweenTiny)
var distanceProgress = reversed ? 1 - timeProgress : timeProgress;
double time = spanStartTime + timeProgress * spanDuration;
if (LegacyLastTickOffset != null)
{
// If we're the last tick, apply the legacy offset
if (span == this.SpanCount() - 1 && isLastTick)
time = Math.Max(StartTime + Duration / 2, time - LegacyLastTickOffset.Value);
}
int tinyTickCount = 1;
double tinyTickInterval = time - lastTickTime;
while (tinyTickInterval > 100 && tinyTickCount < 10000)
{
tinyTickInterval /= 2;
tinyTickCount *= 2;
}
for (int tinyTickIndex = 0; tinyTickIndex < tinyTickCount - 1; tinyTickIndex++)
{
var t = lastTickTime + (tinyTickIndex + 1) * tinyTickInterval;
double progress = reversed ? 1 - (t - spanStartTime) / spanDuration : (t - spanStartTime) / spanDuration;
AddNested(new TinyDroplet
{ {
StartTime = t, AddNested(new TinyDroplet
X = X + Path.PositionAt(progress).X / CatchPlayfield.BASE_WIDTH, {
Samples = tickSamples Samples = tickSamples,
}); StartTime = t + lastEvent.Value.Time,
X = X + Path.PositionAt(
lastEvent.Value.PathProgress + (t / sinceLastTick) * (e.PathProgress - lastEvent.Value.PathProgress)).X / CatchPlayfield.BASE_WIDTH,
});
}
} }
lastTickTime = time;
if (isLastTick)
break;
AddNested(new Droplet
{
StartTime = time,
X = X + Path.PositionAt(distanceProgress).X / CatchPlayfield.BASE_WIDTH,
Samples = tickSamples
});
} }
AddNested(new Fruit // this also includes LegacyLastTick and this is used for TinyDroplet generation above.
// this means that the final segment of TinyDroplets are increasingly mistimed where LegacyLastTickOffset is being applied.
lastEvent = e;
switch (e.Type)
{ {
Samples = Samples, case SliderEventType.Tick:
StartTime = spanStartTime + spanDuration, AddNested(new Droplet
X = X + Path.PositionAt(reversed ? 0 : 1).X / CatchPlayfield.BASE_WIDTH {
}); Samples = tickSamples,
StartTime = e.Time,
X = X + Path.PositionAt(e.PathProgress).X / CatchPlayfield.BASE_WIDTH,
});
break;
case SliderEventType.Head:
case SliderEventType.Tail:
case SliderEventType.Repeat:
AddNested(new Fruit
{
Samples = Samples,
StartTime = e.Time,
X = X + Path.PositionAt(e.PathProgress).X / CatchPlayfield.BASE_WIDTH,
});
break;
}
} }
} }

View File

@ -6,17 +6,20 @@ using System.Linq;
using osu.Framework.MathUtils; using osu.Framework.MathUtils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Replays; using osu.Game.Replays;
using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays;
namespace osu.Game.Rulesets.Catch.Replays namespace osu.Game.Rulesets.Catch.Replays
{ {
internal class CatchAutoGenerator : AutoGenerator<CatchHitObject> internal class CatchAutoGenerator : AutoGenerator
{ {
public const double RELEASE_DELAY = 20; public const double RELEASE_DELAY = 20;
public CatchAutoGenerator(Beatmap<CatchHitObject> beatmap) public new CatchBeatmap Beatmap => (CatchBeatmap)base.Beatmap;
public CatchAutoGenerator(IBeatmap beatmap)
: base(beatmap) : base(beatmap)
{ {
Replay = new Replay(); Replay = new Replay();

View File

@ -12,6 +12,7 @@ using System.Linq;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Replays; using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Replays.Types; using osu.Game.Rulesets.Replays.Types;
using osu.Game.Beatmaps.Legacy; using osu.Game.Beatmaps.Legacy;
@ -145,6 +146,11 @@ namespace osu.Game.Rulesets.Mania
{ {
new MultiMod(new ManiaModAutoplay(), new ModCinema()), new MultiMod(new ManiaModAutoplay(), new ModCinema()),
}; };
case ModType.Fun:
return new Mod[]
{
new MultiMod(new ModWindUp<ManiaHitObject>(), new ModWindDown<ManiaHitObject>())
};
default: default:
return new Mod[] { }; return new Mod[] { };
} }

View File

@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Mania.Mods
{ {
public class ManiaModAutoplay : ModAutoplay<ManiaHitObject> public class ManiaModAutoplay : ModAutoplay<ManiaHitObject>
{ {
protected override Score CreateReplayScore(Beatmap<ManiaHitObject> beatmap) => new Score public override Score CreateReplayScore(IBeatmap beatmap) => new Score
{ {
ScoreInfo = new ScoreInfo { User = new User { Username = "osu!topus!" } }, ScoreInfo = new ScoreInfo { User = new User { Username = "osu!topus!" } },
Replay = new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(), Replay = new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(),

View File

@ -5,13 +5,12 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Replays; using osu.Game.Replays;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays;
namespace osu.Game.Rulesets.Mania.Replays namespace osu.Game.Rulesets.Mania.Replays
{ {
internal class ManiaAutoGenerator : AutoGenerator<ManiaHitObject> internal class ManiaAutoGenerator : AutoGenerator
{ {
public const double RELEASE_DELAY = 20; public const double RELEASE_DELAY = 20;

View File

@ -16,18 +16,18 @@ namespace osu.Game.Rulesets.Osu.Tests
[TestFixture] [TestFixture]
public class TestCaseGameplayCursor : OsuTestCase, IProvideCursor public class TestCaseGameplayCursor : OsuTestCase, IProvideCursor
{ {
private GameplayCursor cursor; private GameplayCursorContainer cursorContainer;
public override IReadOnlyList<Type> RequiredTypes => new[] { typeof(CursorTrail) }; public override IReadOnlyList<Type> RequiredTypes => new[] { typeof(CursorTrail) };
public CursorContainer Cursor => cursor; public CursorContainer Cursor => cursorContainer;
public bool ProvidingUserCursor => true; public bool ProvidingUserCursor => true;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
Add(cursor = new GameplayCursor { RelativeSizeAxes = Axes.Both }); Add(cursorContainer = new GameplayCursorContainer { RelativeSizeAxes = Axes.Both });
} }
} }
} }

View File

@ -0,0 +1,16 @@
// 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 NUnit.Framework;
namespace osu.Game.Rulesets.Osu.Tests
{
[TestFixture]
public class TestCaseOsuPlayer : Game.Tests.Visual.TestCasePlayer
{
public TestCaseOsuPlayer()
: base(new OsuRuleset())
{
}
}
}

View File

@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
path = new SmoothPath path = new SmoothPath
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
PathWidth = 1 PathRadius = 1
}, },
marker = new CircularContainer marker = new CircularContainer
{ {

View File

@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
InternalChild = body = new ManualSliderBody InternalChild = body = new ManualSliderBody
{ {
AccentColour = Color4.Transparent, AccentColour = Color4.Transparent,
PathWidth = slider.Scale * 64 PathRadius = slider.Scale * 64
}; };
} }
@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
body.BorderColour = colours.Yellow; body.BorderColour = colours.Yellow;
PositionBindable.BindValueChanged(_ => updatePosition(), true); PositionBindable.BindValueChanged(_ => updatePosition(), true);
ScaleBindable.BindValueChanged(scale => body.PathWidth = scale.NewValue * 64, true); ScaleBindable.BindValueChanged(scale => body.PathRadius = scale.NewValue * 64, true);
} }
private void updatePosition() => Position = slider.StackedPosition; private void updatePosition() => Position = slider.StackedPosition;

View File

@ -1,7 +1,6 @@
// 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 osu.Framework.Graphics.Cursor;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
@ -16,8 +15,14 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
} }
protected override CursorContainer CreateCursor() => null; protected override Playfield CreatePlayfield() => new OsuPlayfieldNoCursor { Size = Vector2.One };
protected override Playfield CreatePlayfield() => new OsuPlayfield { Size = Vector2.One }; private class OsuPlayfieldNoCursor : OsuPlayfield
{
public OsuPlayfieldNoCursor()
{
Cursor?.Expire();
}
}
} }
} }

View File

@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).Append(typeof(OsuModSpunOut)).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).Append(typeof(OsuModSpunOut)).ToArray();
protected override Score CreateReplayScore(Beatmap<OsuHitObject> beatmap) => new Score public override Score CreateReplayScore(IBeatmap beatmap) => new Score
{ {
ScoreInfo = new ScoreInfo { User = new User { Username = "Autoplay" } }, ScoreInfo = new ScoreInfo { User = new User { Username = "Autoplay" } },
Replay = new OsuAutoGenerator(beatmap).Generate() Replay = new OsuAutoGenerator(beatmap).Generate()

View File

@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
Body = new SnakingSliderBody(s) Body = new SnakingSliderBody(s)
{ {
PathWidth = s.Scale * 64, PathRadius = s.Scale * 64,
}, },
ticks = new Container<DrawableSliderTick> { RelativeSizeAxes = Axes.Both }, ticks = new Container<DrawableSliderTick> { RelativeSizeAxes = Axes.Both },
repeatPoints = new Container<DrawableRepeatPoint> { RelativeSizeAxes = Axes.Both }, repeatPoints = new Container<DrawableRepeatPoint> { RelativeSizeAxes = Axes.Both },
@ -105,7 +105,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
positionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition); positionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition);
scaleBindable.BindValueChanged(scale => scaleBindable.BindValueChanged(scale =>
{ {
Body.PathWidth = scale.NewValue * 64; Body.PathRadius = scale.NewValue * 64;
Ball.Scale = new Vector2(scale.NewValue); Ball.Scale = new Vector2(scale.NewValue);
}); });

View File

@ -19,10 +19,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
private readonly BufferedContainer container; private readonly BufferedContainer container;
public float PathWidth public float PathRadius
{ {
get => path.PathWidth; get => path.PathRadius;
set => path.PathWidth = value; set => path.PathRadius = value;
} }
/// <summary> /// <summary>

View File

@ -1,7 +1,6 @@
// 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 osuTK; using osuTK;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using System.Collections.Generic; using System.Collections.Generic;
@ -155,116 +154,76 @@ namespace osu.Game.Rulesets.Osu.Objects
{ {
base.CreateNestedHitObjects(); base.CreateNestedHitObjects();
createSliderEnds(); foreach (var e in
createTicks(); SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset))
createRepeatPoints();
if (LegacyLastTickOffset != null)
TailCircle.StartTime = Math.Max(StartTime + Duration / 2, TailCircle.StartTime - LegacyLastTickOffset.Value);
}
private void createSliderEnds()
{
HeadCircle = new SliderCircle
{ {
StartTime = StartTime, var firstSample = Samples.Find(s => s.Name == SampleInfo.HIT_NORMAL)
Position = Position, ?? Samples.FirstOrDefault(); // TODO: remove this when guaranteed sort is present for samples (https://github.com/ppy/osu/issues/1933)
Samples = getNodeSamples(0), var sampleList = new List<SampleInfo>();
SampleControlPoint = SampleControlPoint,
IndexInCurrentCombo = IndexInCurrentCombo,
ComboIndex = ComboIndex,
};
TailCircle = new SliderTailCircle(this) if (firstSample != null)
{ sampleList.Add(new SampleInfo
StartTime = EndTime,
Position = EndPosition,
IndexInCurrentCombo = IndexInCurrentCombo,
ComboIndex = ComboIndex,
};
AddNested(HeadCircle);
AddNested(TailCircle);
}
private void createTicks()
{
// A very lenient maximum length of a slider for ticks to be generated.
// This exists for edge cases such as /b/1573664 where the beatmap has been edited by the user, and should never be reached in normal usage.
const double max_length = 100000;
var length = Math.Min(max_length, Path.Distance);
var tickDistance = MathHelper.Clamp(TickDistance, 0, length);
if (tickDistance == 0) return;
var minDistanceFromEnd = Velocity * 10;
var spanCount = this.SpanCount();
for (var span = 0; span < spanCount; span++)
{
var spanStartTime = StartTime + span * SpanDuration;
var reversed = span % 2 == 1;
for (var d = tickDistance; d <= length; d += tickDistance)
{
if (d > length - minDistanceFromEnd)
break;
var distanceProgress = d / length;
var timeProgress = reversed ? 1 - distanceProgress : distanceProgress;
var firstSample = Samples.Find(s => s.Name == SampleInfo.HIT_NORMAL)
?? Samples.FirstOrDefault(); // TODO: remove this when guaranteed sort is present for samples (https://github.com/ppy/osu/issues/1933)
var sampleList = new List<SampleInfo>();
if (firstSample != null)
sampleList.Add(new SampleInfo
{
Bank = firstSample.Bank,
Volume = firstSample.Volume,
Name = @"slidertick",
});
AddNested(new SliderTick
{ {
SpanIndex = span, Bank = firstSample.Bank,
SpanStartTime = spanStartTime, Volume = firstSample.Volume,
StartTime = spanStartTime + timeProgress * SpanDuration, Name = @"slidertick",
Position = Position + Path.PositionAt(distanceProgress),
StackHeight = StackHeight,
Scale = Scale,
Samples = sampleList
}); });
switch (e.Type)
{
case SliderEventType.Tick:
AddNested(new SliderTick
{
SpanIndex = e.SpanIndex,
SpanStartTime = e.SpanStartTime,
StartTime = e.Time,
Position = Position + Path.PositionAt(e.PathProgress),
StackHeight = StackHeight,
Scale = Scale,
Samples = sampleList
});
break;
case SliderEventType.Head:
AddNested(HeadCircle = new SliderCircle
{
StartTime = e.Time,
Position = Position,
Samples = getNodeSamples(0),
SampleControlPoint = SampleControlPoint,
IndexInCurrentCombo = IndexInCurrentCombo,
ComboIndex = ComboIndex,
});
break;
case SliderEventType.LegacyLastTick:
// we need to use the LegacyLastTick here for compatibility reasons (difficulty).
// it is *okay* to use this because the TailCircle is not used for any meaningful purpose in gameplay.
// if this is to change, we should revisit this.
AddNested(TailCircle = new SliderTailCircle(this)
{
StartTime = e.Time,
Position = EndPosition,
IndexInCurrentCombo = IndexInCurrentCombo,
ComboIndex = ComboIndex,
});
break;
case SliderEventType.Repeat:
AddNested(new RepeatPoint
{
RepeatIndex = e.SpanIndex,
SpanDuration = SpanDuration,
StartTime = StartTime + (e.SpanIndex + 1) * SpanDuration,
Position = Position + Path.PositionAt(e.PathProgress),
StackHeight = StackHeight,
Scale = Scale,
Samples = getNodeSamples(e.SpanIndex + 1)
});
break;
} }
} }
} }
private void createRepeatPoints() private List<SampleInfo> getNodeSamples(int nodeIndex) =>
{ nodeIndex < NodeSamples.Count ? NodeSamples[nodeIndex] : Samples;
for (int repeatIndex = 0, repeat = 1; repeatIndex < RepeatCount; repeatIndex++, repeat++)
{
AddNested(new RepeatPoint
{
RepeatIndex = repeatIndex,
SpanDuration = SpanDuration,
StartTime = StartTime + repeat * SpanDuration,
Position = Position + Path.PositionAt(repeat % 2),
StackHeight = StackHeight,
Scale = Scale,
Samples = getNodeSamples(1 + repeatIndex)
});
}
}
private List<SampleInfo> getNodeSamples(int nodeIndex)
{
if (nodeIndex < NodeSamples.Count)
return NodeSamples[nodeIndex];
return Samples;
}
public override Judgement CreateJudgement() => new OsuJudgement(); public override Judgement CreateJudgement() => new OsuJudgement();
} }

View File

@ -8,6 +8,10 @@ using osu.Game.Rulesets.Osu.Judgements;
namespace osu.Game.Rulesets.Osu.Objects namespace osu.Game.Rulesets.Osu.Objects
{ {
/// <summary>
/// Note that this should not be used for timing correctness.
/// See <see cref="SliderEventType.LegacyLastTick"/> usage in <see cref="Slider"/> for more information.
/// </summary>
public class SliderTailCircle : SliderCircle public class SliderTailCircle : SliderCircle
{ {
private readonly IBindable<SliderPath> pathBindable = new Bindable<SliderPath>(); private readonly IBindable<SliderPath> pathBindable = new Bindable<SliderPath>();

View File

@ -13,6 +13,7 @@ using osu.Game.Overlays.Settings;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Game.Rulesets.Osu.Edit; using osu.Game.Rulesets.Osu.Edit;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Replays.Types; using osu.Game.Rulesets.Replays.Types;
using osu.Game.Beatmaps.Legacy; using osu.Game.Beatmaps.Legacy;
@ -128,7 +129,8 @@ namespace osu.Game.Rulesets.Osu
{ {
new OsuModTransform(), new OsuModTransform(),
new OsuModWiggle(), new OsuModWiggle(),
new OsuModGrow() new OsuModGrow(),
new MultiMod(new ModWindUp<OsuHitObject>(), new ModWindDown<OsuHitObject>()),
}; };
default: default:
return new Mod[] { }; return new Mod[] { };

View File

@ -10,12 +10,15 @@ using System.Linq;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Replays; using osu.Game.Replays;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Replays namespace osu.Game.Rulesets.Osu.Replays
{ {
public class OsuAutoGenerator : OsuAutoGeneratorBase public class OsuAutoGenerator : OsuAutoGeneratorBase
{ {
public new OsuBeatmap Beatmap => (OsuBeatmap)base.Beatmap;
#region Parameters #region Parameters
/// <summary> /// <summary>
@ -42,7 +45,7 @@ namespace osu.Game.Rulesets.Osu.Replays
#region Construction / Initialisation #region Construction / Initialisation
public OsuAutoGenerator(Beatmap<OsuHitObject> beatmap) public OsuAutoGenerator(IBeatmap beatmap)
: base(beatmap) : base(beatmap)
{ {
// Already superhuman, but still somewhat realistic // Already superhuman, but still somewhat realistic

View File

@ -3,7 +3,6 @@
using osuTK; using osuTK;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Game.Replays; using osu.Game.Replays;
@ -12,7 +11,7 @@ using osu.Game.Rulesets.Replays;
namespace osu.Game.Rulesets.Osu.Replays namespace osu.Game.Rulesets.Osu.Replays
{ {
public abstract class OsuAutoGeneratorBase : AutoGenerator<OsuHitObject> public abstract class OsuAutoGeneratorBase : AutoGenerator
{ {
#region Constants #region Constants
@ -35,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Replays
protected Replay Replay; protected Replay Replay;
protected List<ReplayFrame> Frames => Replay.Frames; protected List<ReplayFrame> Frames => Replay.Frames;
protected OsuAutoGeneratorBase(Beatmap<OsuHitObject> beatmap) protected OsuAutoGeneratorBase(IBeatmap beatmap)
: base(beatmap) : base(beatmap)
{ {
Replay = new Replay(); Replay = new Replay();

View File

@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
{ {
private int currentIndex; private int currentIndex;
private Shader shader; private IShader shader;
private Texture texture; private Texture texture;
private Vector2 size => texture.Size * Scale; private Vector2 size => texture.Size * Scale;
@ -35,7 +35,6 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
public override bool IsPresent => true; public override bool IsPresent => true;
private readonly TrailDrawNodeSharedData trailDrawNodeSharedData = new TrailDrawNodeSharedData();
private const int max_sprites = 2048; private const int max_sprites = 2048;
private readonly TrailPart[] parts = new TrailPart[max_sprites]; private readonly TrailPart[] parts = new TrailPart[max_sprites];
@ -55,7 +54,6 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
tNode.Texture = texture; tNode.Texture = texture;
tNode.Size = size; tNode.Size = size;
tNode.Time = time; tNode.Time = time;
tNode.Shared = trailDrawNodeSharedData;
for (int i = 0; i < parts.Length; ++i) for (int i = 0; i < parts.Length; ++i)
if (parts[i].InvalidationID > tNode.Parts[i].InvalidationID) if (parts[i].InvalidationID > tNode.Parts[i].InvalidationID)
@ -81,7 +79,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(ShaderManager shaders, TextureStore textures) private void load(ShaderManager shaders, TextureStore textures)
{ {
shader = shaders?.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE); shader = shaders.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE);
texture = textures.Get(@"Cursor/cursortrail"); texture = textures.Get(@"Cursor/cursortrail");
Scale = new Vector2(1 / texture.ScaleAdjust); Scale = new Vector2(1 / texture.ScaleAdjust);
} }
@ -167,22 +165,18 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
public bool WasUpdated; public bool WasUpdated;
} }
private class TrailDrawNodeSharedData
{
public VertexBuffer<TexturedTrailVertex> VertexBuffer;
}
private class TrailDrawNode : DrawNode private class TrailDrawNode : DrawNode
{ {
public Shader Shader; public IShader Shader;
public Texture Texture; public Texture Texture;
public float Time; public float Time;
public TrailDrawNodeSharedData Shared;
public readonly TrailPart[] Parts = new TrailPart[max_sprites]; public readonly TrailPart[] Parts = new TrailPart[max_sprites];
public Vector2 Size; public Vector2 Size;
private readonly VertexBuffer<TexturedTrailVertex> vertexBuffer = new QuadVertexBuffer<TexturedTrailVertex>(max_sprites, BufferUsageHint.DynamicDraw);
public TrailDrawNode() public TrailDrawNode()
{ {
for (int i = 0; i < max_sprites; i++) for (int i = 0; i < max_sprites; i++)
@ -194,9 +188,6 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
public override void Draw(Action<TexturedVertex2D> vertexAction) public override void Draw(Action<TexturedVertex2D> vertexAction)
{ {
if (Shared.VertexBuffer == null)
Shared.VertexBuffer = new QuadVertexBuffer<TexturedTrailVertex>(max_sprites, BufferUsageHint.DynamicDraw);
Shader.GetUniform<float>("g_FadeClock").UpdateValue(ref Time); Shader.GetUniform<float>("g_FadeClock").UpdateValue(ref Time);
int updateStart = -1, updateEnd = 0; int updateStart = -1, updateEnd = 0;
@ -218,7 +209,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
new Quad(pos.X - Size.X / 2, pos.Y - Size.Y / 2, Size.X, Size.Y), new Quad(pos.X - Size.X / 2, pos.Y - Size.Y / 2, Size.X, Size.Y),
DrawColourInfo.Colour, DrawColourInfo.Colour,
null, null,
v => Shared.VertexBuffer.Vertices[end++] = new TexturedTrailVertex v => vertexBuffer.Vertices[end++] = new TexturedTrailVertex
{ {
Position = v.Position, Position = v.Position,
TexturePosition = v.TexturePosition, TexturePosition = v.TexturePosition,
@ -230,24 +221,31 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
} }
else if (updateStart != -1) else if (updateStart != -1)
{ {
Shared.VertexBuffer.UpdateRange(updateStart * 4, updateEnd * 4); vertexBuffer.UpdateRange(updateStart * 4, updateEnd * 4);
updateStart = -1; updateStart = -1;
} }
} }
// Update all remaining vertices that have been changed. // Update all remaining vertices that have been changed.
if (updateStart != -1) if (updateStart != -1)
Shared.VertexBuffer.UpdateRange(updateStart * 4, updateEnd * 4); vertexBuffer.UpdateRange(updateStart * 4, updateEnd * 4);
base.Draw(vertexAction); base.Draw(vertexAction);
Shader.Bind(); Shader.Bind();
Texture.TextureGL.Bind(); Texture.TextureGL.Bind();
Shared.VertexBuffer.Draw(); vertexBuffer.Draw();
Shader.Unbind(); Shader.Unbind();
} }
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
vertexBuffer.Dispose();
}
} }
[StructLayout(LayoutKind.Sequential)] [StructLayout(LayoutKind.Sequential)]

View File

@ -17,7 +17,7 @@ using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.UI.Cursor namespace osu.Game.Rulesets.Osu.UI.Cursor
{ {
public class GameplayCursor : CursorContainer, IKeyBindingHandler<OsuAction> public class GameplayCursorContainer : CursorContainer, IKeyBindingHandler<OsuAction>
{ {
protected override Drawable CreateCursor() => new OsuCursor(); protected override Drawable CreateCursor() => new OsuCursor();
@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private readonly Container<Drawable> fadeContainer; private readonly Container<Drawable> fadeContainer;
public GameplayCursor() public GameplayCursorContainer()
{ {
InternalChild = fadeContainer = new Container InternalChild = fadeContainer = new Container
{ {
@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
public OsuCursor() public OsuCursor()
{ {
Origin = Anchor.Centre; Origin = Anchor.Centre;
Size = new Vector2(42); Size = new Vector2(28);
} }
protected override void SkinChanged(ISkinSource skin, bool allowFallback) protected override void SkinChanged(ISkinSource skin, bool allowFallback)

View File

@ -10,7 +10,9 @@ using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables.Connections; using osu.Game.Rulesets.Osu.Objects.Drawables.Connections;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using System.Linq; using System.Linq;
using osu.Framework.Graphics.Cursor;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.UI.Cursor;
namespace osu.Game.Rulesets.Osu.UI namespace osu.Game.Rulesets.Osu.UI
{ {
@ -22,6 +24,12 @@ namespace osu.Game.Rulesets.Osu.UI
public static readonly Vector2 BASE_SIZE = new Vector2(512, 384); public static readonly Vector2 BASE_SIZE = new Vector2(512, 384);
private readonly PlayfieldAdjustmentContainer adjustmentContainer;
protected override Container CursorTargetContainer => adjustmentContainer;
protected override CursorContainer CreateCursor() => new GameplayCursorContainer();
public OsuPlayfield() public OsuPlayfield()
{ {
Anchor = Anchor.Centre; Anchor = Anchor.Centre;
@ -29,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.UI
Size = new Vector2(0.75f); Size = new Vector2(0.75f);
InternalChild = new PlayfieldAdjustmentContainer InternalChild = adjustmentContainer = new PlayfieldAdjustmentContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Children = new Drawable[] Children = new Drawable[]

View File

@ -2,7 +2,6 @@
// 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.Framework.Graphics.Cursor;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Input.Handlers; using osu.Game.Input.Handlers;
@ -13,7 +12,6 @@ using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
@ -59,7 +57,5 @@ namespace osu.Game.Rulesets.Osu.UI
return first.StartTime - first.TimePreempt; return first.StartTime - first.TimePreempt;
} }
} }
protected override CursorContainer CreateCursor() => new GameplayCursor();
} }
} }

View File

@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
{ {
public class TaikoModAutoplay : ModAutoplay<TaikoHitObject> public class TaikoModAutoplay : ModAutoplay<TaikoHitObject>
{ {
protected override Score CreateReplayScore(Beatmap<TaikoHitObject> beatmap) => new Score public override Score CreateReplayScore(IBeatmap beatmap) => new Score
{ {
ScoreInfo = new ScoreInfo { User = new User { Username = "mekkadosu!" } }, ScoreInfo = new ScoreInfo { User = new User { Username = "mekkadosu!" } },
Replay = new TaikoAutoGenerator(beatmap).Generate(), Replay = new TaikoAutoGenerator(beatmap).Generate(),

View File

@ -9,14 +9,17 @@ using osu.Game.Replays;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Taiko.Beatmaps;
namespace osu.Game.Rulesets.Taiko.Replays namespace osu.Game.Rulesets.Taiko.Replays
{ {
public class TaikoAutoGenerator : AutoGenerator<TaikoHitObject> public class TaikoAutoGenerator : AutoGenerator
{ {
public new TaikoBeatmap Beatmap => (TaikoBeatmap)base.Beatmap;
private const double swell_hit_speed = 50; private const double swell_hit_speed = 50;
public TaikoAutoGenerator(Beatmap<TaikoHitObject> beatmap) public TaikoAutoGenerator(IBeatmap beatmap)
: base(beatmap) : base(beatmap)
{ {
Replay = new Replay(); Replay = new Replay();

View File

@ -11,6 +11,7 @@ using System.Collections.Generic;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Game.Rulesets.Replays.Types; using osu.Game.Rulesets.Replays.Types;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Replays; using osu.Game.Rulesets.Taiko.Replays;
using osu.Game.Beatmaps.Legacy; using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
@ -99,6 +100,11 @@ namespace osu.Game.Rulesets.Taiko
new MultiMod(new TaikoModAutoplay(), new ModCinema()), new MultiMod(new TaikoModAutoplay(), new ModCinema()),
new TaikoModRelax(), new TaikoModRelax(),
}; };
case ModType.Fun:
return new Mod[]
{
new MultiMod(new ModWindUp<TaikoHitObject>(), new ModWindDown<TaikoHitObject>())
};
default: default:
return new Mod[] { }; return new Mod[] { };
} }

View File

@ -113,6 +113,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
[TestCase(normal)] [TestCase(normal)]
[TestCase(marathon)] [TestCase(marathon)]
[Ignore("temporarily disabled pending DeepEqual fix (https://github.com/jamesfoster/DeepEqual/pull/35)")]
// Currently fails: // Currently fails:
// [TestCase(with_sb)] // [TestCase(with_sb)]
public void TestParity(string beatmap) public void TestParity(string beatmap)

View File

@ -207,6 +207,41 @@ namespace osu.Game.Tests.Beatmaps.IO
} }
} }
[TestCase(true)]
[TestCase(false)]
public void TestImportThenDeleteThenImportWithOnlineIDMismatch(bool set)
{
//unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost($"TestImportThenDeleteThenImport-{set}"))
{
try
{
var osu = loadOsu(host);
var imported = LoadOszIntoOsu(osu);
if (set)
imported.OnlineBeatmapSetID = 1234;
else
imported.Beatmaps.First().OnlineBeatmapID = 1234;
osu.Dependencies.Get<BeatmapManager>().Update(imported);
deleteBeatmapSet(imported, osu);
var importedSecondTime = LoadOszIntoOsu(osu);
// check the newly "imported" beatmap has been reimported due to mismatch (even though hashes matched)
Assert.IsTrue(imported.ID != importedSecondTime.ID);
Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Beatmaps.First().ID);
}
finally
{
host.Exit();
}
}
}
[Test] [Test]
[NonParallelizable] [NonParallelizable]
[Ignore("Binding IPC on Appveyor isn't working (port in use). Need to figure out why")] [Ignore("Binding IPC on Appveyor isn't working (port in use). Need to figure out why")]

View File

@ -1,10 +1,12 @@
// 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.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
namespace osu.Game.Tests.Visual namespace osu.Game.Tests.Visual
@ -14,15 +16,27 @@ namespace osu.Game.Tests.Visual
{ {
protected override Player CreatePlayer(Ruleset ruleset) protected override Player CreatePlayer(Ruleset ruleset)
{ {
// We create a dummy RulesetContainer just to get the replay - we don't want to use mods here var beatmap = Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo);
// to simulate setting a replay rather than having the replay already set for us
Beatmap.Value.Mods.Value = Beatmap.Value.Mods.Value.Concat(new[] { ruleset.GetAutoplayMod() });
var dummyRulesetContainer = ruleset.CreateRulesetContainerWith(Beatmap.Value);
// Reset the mods return new ScoreAccessibleReplayPlayer(ruleset.GetAutoplayMod().CreateReplayScore(beatmap));
Beatmap.Value.Mods.Value = Beatmap.Value.Mods.Value.Where(m => !(m is ModAutoplay)); }
return new ReplayPlayer(dummyRulesetContainer.ReplayScore); protected override void AddCheckSteps(Func<Player> player)
{
base.AddCheckSteps(player);
AddUntilStep(() => ((ScoreAccessibleReplayPlayer)player()).ScoreProcessor.TotalScore.Value > 0, "score above zero");
AddUntilStep(() => ((ScoreAccessibleReplayPlayer)player()).HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 0), "key counter counted keys");
}
private class ScoreAccessibleReplayPlayer : ReplayPlayer
{
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
public new HUDOverlay HUDOverlay => base.HUDOverlay;
public ScoreAccessibleReplayPlayer(Score score)
: base(score)
{
}
} }
} }
} }

View File

@ -24,10 +24,13 @@ namespace osu.Game.Tests.Visual
public TestCaseToolbar() public TestCaseToolbar()
{ {
var toolbar = new Toolbar { State = Visibility.Visible }; var toolbar = new Toolbar { State = Visibility.Visible };
ToolbarNotificationButton notificationButton = null;
Add(toolbar); AddStep("create toolbar", () =>
{
var notificationButton = toolbar.Children.OfType<FillFlowContainer>().Last().Children.OfType<ToolbarNotificationButton>().First(); Add(toolbar);
notificationButton = toolbar.Children.OfType<FillFlowContainer>().Last().Children.OfType<ToolbarNotificationButton>().First();
});
void setNotifications(int count) => AddStep($"set notification count to {count}", () => notificationButton.NotificationCount.Value = count); void setNotifications(int count) => AddStep($"set notification count to {count}", () => notificationButton.NotificationCount.Value = count);

View File

@ -16,7 +16,7 @@ namespace osu.Game.Tests.Visual
{ {
public class TestCaseUpdateableBeatmapBackgroundSprite : OsuTestCase public class TestCaseUpdateableBeatmapBackgroundSprite : OsuTestCase
{ {
private UpdateableBeatmapBackgroundSprite backgroundSprite; private TestUpdateableBeatmapBackgroundSprite backgroundSprite;
[Resolved] [Resolved]
private BeatmapManager beatmaps { get; set; } private BeatmapManager beatmaps { get; set; }
@ -28,30 +28,36 @@ namespace osu.Game.Tests.Visual
var imported = ImportBeatmapTest.LoadOszIntoOsu(osu); var imported = ImportBeatmapTest.LoadOszIntoOsu(osu);
Child = backgroundSprite = new UpdateableBeatmapBackgroundSprite { RelativeSizeAxes = Axes.Both }; Child = backgroundSprite = new TestUpdateableBeatmapBackgroundSprite { RelativeSizeAxes = Axes.Both };
backgroundSprite.Beatmap.BindTo(beatmapBindable); backgroundSprite.Beatmap.BindTo(beatmapBindable);
var req = new GetBeatmapSetRequest(1); var req = new GetBeatmapSetRequest(1);
api.Queue(req); api.Queue(req);
AddStep("null", () => beatmapBindable.Value = null); AddStep("load null beatmap", () => beatmapBindable.Value = null);
AddUntilStep(() => backgroundSprite.ChildCount == 1, "wait for cleanup...");
AddStep("imported", () => beatmapBindable.Value = imported.Beatmaps.First()); AddStep("load imported beatmap", () => beatmapBindable.Value = imported.Beatmaps.First());
AddUntilStep(() => backgroundSprite.ChildCount == 1, "wait for cleanup...");
if (api.IsLoggedIn) if (api.IsLoggedIn)
{ {
AddUntilStep(() => req.Result != null, "wait for api response"); AddUntilStep(() => req.Result != null, "wait for api response");
AddStep("load online beatmap", () => beatmapBindable.Value = new BeatmapInfo
AddStep("online", () => beatmapBindable.Value = new BeatmapInfo
{ {
BeatmapSet = req.Result?.ToBeatmapSet(rulesets) BeatmapSet = req.Result?.ToBeatmapSet(rulesets)
}); });
AddUntilStep(() => backgroundSprite.ChildCount == 1, "wait for cleanup...");
} }
else else
{ {
AddStep("online (login first)", () => { }); AddStep("online (login first)", () => { });
} }
} }
private class TestUpdateableBeatmapBackgroundSprite : UpdateableBeatmapBackgroundSprite
{
public int ChildCount => InternalChildren.Count;
}
} }
} }

View File

@ -102,11 +102,14 @@ namespace osu.Game.Beatmaps
b.BeatmapSet = beatmapSet; b.BeatmapSet = beatmapSet;
} }
validateOnlineIds(beatmapSet.Beatmaps); validateOnlineIds(beatmapSet);
foreach (BeatmapInfo b in beatmapSet.Beatmaps) foreach (BeatmapInfo b in beatmapSet.Beatmaps)
fetchAndPopulateOnlineValues(b, beatmapSet.Beatmaps); fetchAndPopulateOnlineValues(b, beatmapSet.Beatmaps);
}
protected override void PreImport(BeatmapSetInfo beatmapSet)
{
// check if a set already exists with the same online id, delete if it does. // check if a set already exists with the same online id, delete if it does.
if (beatmapSet.OnlineBeatmapSetID != null) if (beatmapSet.OnlineBeatmapSetID != null)
{ {
@ -120,14 +123,30 @@ namespace osu.Game.Beatmaps
} }
} }
private void validateOnlineIds(List<BeatmapInfo> beatmaps) private void validateOnlineIds(BeatmapSetInfo beatmapSet)
{ {
var beatmapIds = beatmaps.Where(b => b.OnlineBeatmapID.HasValue).Select(b => b.OnlineBeatmapID).ToList(); var beatmapIds = beatmapSet.Beatmaps.Where(b => b.OnlineBeatmapID.HasValue).Select(b => b.OnlineBeatmapID).ToList();
// ensure all IDs are unique in this set and none match existing IDs in the local beatmap store. // ensure all IDs are unique
if (beatmapIds.GroupBy(b => b).Any(g => g.Count() > 1) || QueryBeatmaps(b => beatmapIds.Contains(b.OnlineBeatmapID)).Any()) if (beatmapIds.GroupBy(b => b).Any(g => g.Count() > 1))
// remove all online IDs if any problems were found. {
beatmaps.ForEach(b => b.OnlineBeatmapID = null); resetIds();
return;
}
// find any existing beatmaps in the database that have matching online ids
var existingBeatmaps = QueryBeatmaps(b => beatmapIds.Contains(b.OnlineBeatmapID)).ToList();
if (existingBeatmaps.Count > 0)
{
// reset the import ids (to force a re-fetch) *unless* they match the candidate CheckForExisting set.
// we can ignore the case where the new ids are contained by the CheckForExisting set as it will either be used (import skipped) or deleted.
var existing = CheckForExisting(beatmapSet);
if (existing == null || existingBeatmaps.Any(b => !existing.Beatmaps.Contains(b)))
resetIds();
}
void resetIds() => beatmapSet.Beatmaps.ForEach(b => b.OnlineBeatmapID = null);
} }
/// <summary> /// <summary>
@ -254,6 +273,18 @@ namespace osu.Game.Beatmaps
/// <returns>The first result for the provided query, or null if no results were found.</returns> /// <returns>The first result for the provided query, or null if no results were found.</returns>
public BeatmapSetInfo QueryBeatmapSet(Expression<Func<BeatmapSetInfo, bool>> query) => beatmaps.ConsumableItems.AsNoTracking().FirstOrDefault(query); public BeatmapSetInfo QueryBeatmapSet(Expression<Func<BeatmapSetInfo, bool>> query) => beatmaps.ConsumableItems.AsNoTracking().FirstOrDefault(query);
protected override bool CanUndelete(BeatmapSetInfo existing, BeatmapSetInfo import)
{
if (!base.CanUndelete(existing, import))
return false;
var existingIds = existing.Beatmaps.Select(b => b.OnlineBeatmapID).OrderBy(i => i);
var importIds = import.Beatmaps.Select(b => b.OnlineBeatmapID).OrderBy(i => i);
// force re-import if we are not in a sane state.
return existing.OnlineBeatmapSetID == import.OnlineBeatmapSetID && existingIds.SequenceEqual(importIds);
}
/// <summary> /// <summary>
/// Returns a list of all usable <see cref="BeatmapSetInfo"/>s. /// Returns a list of all usable <see cref="BeatmapSetInfo"/>s.
/// </summary> /// </summary>

View File

@ -9,7 +9,7 @@ using osu.Framework.Graphics.Containers;
namespace osu.Game.Beatmaps.Drawables namespace osu.Game.Beatmaps.Drawables
{ {
/// <summary> /// <summary>
/// Display a baetmap background from a local source, but fallback to online source if not available. /// Display a beatmap background from a local source, but fallback to online source if not available.
/// </summary> /// </summary>
public class UpdateableBeatmapBackgroundSprite : ModelBackedDrawable<BeatmapInfo> public class UpdateableBeatmapBackgroundSprite : ModelBackedDrawable<BeatmapInfo>
{ {
@ -26,37 +26,45 @@ namespace osu.Game.Beatmaps.Drawables
this.beatmapSetCoverType = beatmapSetCoverType; this.beatmapSetCoverType = beatmapSetCoverType;
} }
protected override Drawable CreateDrawable(BeatmapInfo model) private BeatmapInfo lastModel;
protected override DelayedLoadWrapper CreateDelayedLoadWrapper(Drawable content, double timeBeforeLoad)
{ {
return new DelayedLoadUnloadWrapper(() => return new DelayedLoadUnloadWrapper(() =>
{ {
Drawable drawable; // If DelayedLoadUnloadWrapper is attempting to RELOAD the same content (Beatmap), that means that it was
// previously UNLOADED and thus its children have been disposed of, so we need to recreate them here.
if (lastModel == Beatmap.Value && Beatmap.Value != null)
return CreateDrawable(Beatmap.Value);
var localBeatmap = beatmaps.GetWorkingBeatmap(model); // If the model has changed since the previous unload (or if there was no load), then we can safely use the given content
lastModel = Beatmap.Value;
if (model?.BeatmapSet?.OnlineInfo != null) return content;
drawable = new BeatmapSetCover(model.BeatmapSet, beatmapSetCoverType); }, timeBeforeLoad, 10000);
else if (localBeatmap.BeatmapInfo.ID != 0)
{
// Fall back to local background if one exists
drawable = new BeatmapBackgroundSprite(localBeatmap);
}
else
{
// Use the default background if somehow an online set does not exist and we don't have a local copy.
drawable = new BeatmapBackgroundSprite(beatmaps.DefaultBeatmap);
}
drawable.RelativeSizeAxes = Axes.Both;
drawable.Anchor = Anchor.Centre;
drawable.Origin = Anchor.Centre;
drawable.FillMode = FillMode.Fill;
drawable.OnLoadComplete = d => d.FadeInFromZero(400);
return drawable;
}, 500, 10000);
} }
protected override double FadeDuration => 0; protected override Drawable CreateDrawable(BeatmapInfo model)
{
Drawable drawable = getDrawableForModel(model);
drawable.RelativeSizeAxes = Axes.Both;
drawable.Anchor = Anchor.Centre;
drawable.Origin = Anchor.Centre;
drawable.FillMode = FillMode.Fill;
drawable.OnLoadComplete = d => d.FadeInFromZero(400);
return drawable;
}
private Drawable getDrawableForModel(BeatmapInfo model)
{
// prefer online cover where available.
if (model?.BeatmapSet?.OnlineInfo != null)
return new BeatmapSetCover(model.BeatmapSet, beatmapSetCoverType);
return model?.ID > 0
? new BeatmapBackgroundSprite(beatmaps.GetWorkingBeatmap(model))
: new BeatmapBackgroundSprite(beatmaps.DefaultBeatmap);
}
} }
} }

View File

@ -300,21 +300,31 @@ namespace osu.Game.Database
{ {
if (!write.IsTransactionLeader) throw new InvalidOperationException($"Ensure there is no parent transaction so errors can correctly be handled by {this}"); if (!write.IsTransactionLeader) throw new InvalidOperationException($"Ensure there is no parent transaction so errors can correctly be handled by {this}");
var existing = CheckForExisting(item);
if (existing != null)
{
Undelete(existing);
Logger.Log($"Found existing {typeof(TModel)} for {item} (ID {existing.ID}). Skipping import.", LoggingTarget.Database);
handleEvent(() => ItemAdded?.Invoke(existing, true));
return existing;
}
if (archive != null) if (archive != null)
item.Files = createFileInfos(archive, Files); item.Files = createFileInfos(archive, Files);
Populate(item, archive); Populate(item, archive);
var existing = CheckForExisting(item);
if (existing != null)
{
if (CanUndelete(existing, item))
{
Undelete(existing);
Logger.Log($"Found existing {typeof(TModel)} for {item} (ID {existing.ID}). Skipping import.", LoggingTarget.Database);
handleEvent(() => ItemAdded?.Invoke(existing, true));
return existing;
}
else
{
Delete(existing);
ModelStore.PurgeDeletable(s => s.ID == existing.ID);
}
}
PreImport(item);
// import to store // import to store
ModelStore.Add(item); ModelStore.Add(item);
} }
@ -542,12 +552,29 @@ namespace osu.Game.Database
{ {
} }
/// <summary>
/// Perform any final actions before the import to database executes.
/// </summary>
/// <param name="model">The model prepared for import.</param>
protected virtual void PreImport(TModel model)
{
}
/// <summary> /// <summary>
/// Check whether an existing model already exists for a new import item. /// Check whether an existing model already exists for a new import item.
/// </summary> /// </summary>
/// <param name="model">The new model proposed for import. Note that <see cref="Populate"/> has not yet been run on this model.</param> /// <param name="model">The new model proposed for import.
/// <returns>An existing model which matches the criteria to skip importing, else null.</returns> /// <returns>An existing model which matches the criteria to skip importing, else null.</returns>
protected virtual TModel CheckForExisting(TModel model) => model.Hash == null ? null : ModelStore.ConsumableItems.FirstOrDefault(b => b.Hash == model.Hash); protected TModel CheckForExisting(TModel model) => model.Hash == null ? null : ModelStore.ConsumableItems.FirstOrDefault(b => b.Hash == model.Hash);
/// <summary>
/// After an existing <see cref="TModel"/> is found during an import process, the default behaviour is to restore the existing
/// item and skip the import. This method allows changing that behaviour.
/// </summary>
/// <param name="existing">The existing model.</param>
/// <param name="import">The newly imported model.</param>
/// <returns>Whether the existing model should be restored and used. Returning false will delete the existing a force a re-import.</returns>
protected virtual bool CanUndelete(TModel existing, TModel import) => true;
private DbSet<TModel> queryModel() => ContextFactory.Get().Set<TModel>(); private DbSet<TModel> queryModel() => ContextFactory.Get().Set<TModel>();

View File

@ -64,7 +64,7 @@ namespace osu.Game.Graphics.Backgrounds
private readonly SortedList<TriangleParticle> parts = new SortedList<TriangleParticle>(Comparer<TriangleParticle>.Default); private readonly SortedList<TriangleParticle> parts = new SortedList<TriangleParticle>(Comparer<TriangleParticle>.Default);
private Shader shader; private IShader shader;
private readonly Texture texture; private readonly Texture texture;
public Triangles() public Triangles()
@ -75,7 +75,7 @@ namespace osu.Game.Graphics.Backgrounds
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(ShaderManager shaders) private void load(ShaderManager shaders)
{ {
shader = shaders?.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED); shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED);
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -180,8 +180,6 @@ namespace osu.Game.Graphics.Backgrounds
protected override DrawNode CreateDrawNode() => new TrianglesDrawNode(); protected override DrawNode CreateDrawNode() => new TrianglesDrawNode();
private readonly TrianglesDrawNodeSharedData sharedData = new TrianglesDrawNodeSharedData();
protected override void ApplyDrawNode(DrawNode node) protected override void ApplyDrawNode(DrawNode node)
{ {
base.ApplyDrawNode(node); base.ApplyDrawNode(node);
@ -191,27 +189,21 @@ namespace osu.Game.Graphics.Backgrounds
trianglesNode.Shader = shader; trianglesNode.Shader = shader;
trianglesNode.Texture = texture; trianglesNode.Texture = texture;
trianglesNode.Size = DrawSize; trianglesNode.Size = DrawSize;
trianglesNode.Shared = sharedData;
trianglesNode.Parts.Clear(); trianglesNode.Parts.Clear();
trianglesNode.Parts.AddRange(parts); trianglesNode.Parts.AddRange(parts);
} }
private class TrianglesDrawNodeSharedData
{
public readonly LinearBatch<TexturedVertex2D> VertexBatch = new LinearBatch<TexturedVertex2D>(100 * 3, 10, PrimitiveType.Triangles);
}
private class TrianglesDrawNode : DrawNode private class TrianglesDrawNode : DrawNode
{ {
public Shader Shader; public IShader Shader;
public Texture Texture; public Texture Texture;
public TrianglesDrawNodeSharedData Shared;
public readonly List<TriangleParticle> Parts = new List<TriangleParticle>(); public readonly List<TriangleParticle> Parts = new List<TriangleParticle>();
public Vector2 Size; public Vector2 Size;
private readonly LinearBatch<TexturedVertex2D> vertexBatch = new LinearBatch<TexturedVertex2D>(100 * 3, 10, PrimitiveType.Triangles);
public override void Draw(Action<TexturedVertex2D> vertexAction) public override void Draw(Action<TexturedVertex2D> vertexAction)
{ {
base.Draw(vertexAction); base.Draw(vertexAction);
@ -239,12 +231,19 @@ namespace osu.Game.Graphics.Backgrounds
triangle, triangle,
colourInfo, colourInfo,
null, null,
Shared.VertexBatch.AddAction, vertexBatch.AddAction,
Vector2.Divide(localInflationAmount, size)); Vector2.Divide(localInflationAmount, size));
} }
Shader.Unbind(); Shader.Unbind();
} }
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
vertexBatch.Dispose();
}
} }
protected struct TriangleParticle : IComparable<TriangleParticle> protected struct TriangleParticle : IComparable<TriangleParticle>

View File

@ -69,7 +69,7 @@ namespace osu.Game.Graphics.UserInterface
{ {
Masking = true, Masking = true,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Child = path = new SmoothPath { RelativeSizeAxes = Axes.Both, PathWidth = 1 } Child = path = new SmoothPath { RelativeSizeAxes = Axes.Both, PathRadius = 1 }
}); });
} }
@ -102,9 +102,10 @@ namespace osu.Game.Graphics.UserInterface
for (int i = 0; i < values.Length; i++) for (int i = 0; i < values.Length; i++)
{ {
float x = (i + count - values.Length) / (float)(count - 1) * DrawWidth - 1; // Make sure that we are accounting for path width when calculating vertex positions
float y = GetYPosition(values[i]) * DrawHeight - 1; // We need to apply 2x the path radius to account for it because the full diameter of the line accounts into height
// the -1 is for inner offset in path (actually -PathWidth) float x = (i + count - values.Length) / (float)(count - 1) * (DrawWidth - 2 * path.PathRadius);
float y = GetYPosition(values[i]) * (DrawHeight - 2 * path.PathRadius);
path.AddVertex(new Vector2(x, y)); path.AddVertex(new Vector2(x, y));
} }
} }

View File

@ -70,13 +70,15 @@ namespace osu.Game.Online.API
internal new void Schedule(Action action) => base.Schedule(action); internal new void Schedule(Action action) => base.Schedule(action);
/// <summary>
/// Register a component to receive API events.
/// Fires <see cref="IOnlineComponent.APIStateChanged"/> once immediately to ensure a correct state.
/// </summary>
/// <param name="component"></param>
public void Register(IOnlineComponent component) public void Register(IOnlineComponent component)
{ {
Scheduler.Add(delegate Scheduler.Add(delegate { components.Add(component); });
{ component.APIStateChanged(this, state);
components.Add(component);
component.APIStateChanged(this, state);
});
} }
public void Unregister(IOnlineComponent component) public void Unregister(IOnlineComponent component)

View File

@ -124,6 +124,7 @@ namespace osu.Game.Online.Multiplayer
if (other.Host.Value != null && Host.Value?.Id != other.Host.Value.Id) if (other.Host.Value != null && Host.Value?.Id != other.Host.Value.Id)
Host.Value = other.Host.Value; Host.Value = other.Host.Value;
ChannelId.Value = other.ChannelId.Value;
Status.Value = other.Status.Value; Status.Value = other.Status.Value;
Availability.Value = other.Availability.Value; Availability.Value = other.Availability.Value;
Type.Value = other.Type.Value; Type.Value = other.Type.Value;

View File

@ -320,6 +320,8 @@ namespace osu.Game.Overlays
this.MoveToY(Height, transition_length, Easing.InSine); this.MoveToY(Height, transition_length, Easing.InSine);
this.FadeOut(transition_length, Easing.InSine); this.FadeOut(transition_length, Easing.InSine);
channelSelectionOverlay.State = Visibility.Hidden;
textbox.HoldFocus = false; textbox.HoldFocus = false;
base.PopOut(); base.PopOut();
} }

View File

@ -134,9 +134,9 @@ namespace osu.Game.Overlays
Filter.Tabs.Current.Value = DirectSortCriteria.Ranked; Filter.Tabs.Current.Value = DirectSortCriteria.Ranked;
} }
}; };
((FilterControl)Filter).Ruleset.ValueChanged += _ => Scheduler.AddOnce(updateSearch); ((FilterControl)Filter).Ruleset.ValueChanged += _ => queueUpdateSearch();
Filter.DisplayStyleControl.DisplayStyle.ValueChanged += style => recreatePanels(style.NewValue); Filter.DisplayStyleControl.DisplayStyle.ValueChanged += style => recreatePanels(style.NewValue);
Filter.DisplayStyleControl.Dropdown.Current.ValueChanged += _ => Scheduler.AddOnce(updateSearch); Filter.DisplayStyleControl.Dropdown.Current.ValueChanged += _ => queueUpdateSearch();
Header.Tabs.Current.ValueChanged += tab => Header.Tabs.Current.ValueChanged += tab =>
{ {
@ -144,24 +144,11 @@ namespace osu.Game.Overlays
{ {
currentQuery.Value = string.Empty; currentQuery.Value = string.Empty;
Filter.Tabs.Current.Value = (DirectSortCriteria)Header.Tabs.Current.Value; Filter.Tabs.Current.Value = (DirectSortCriteria)Header.Tabs.Current.Value;
Scheduler.AddOnce(updateSearch); queueUpdateSearch();
} }
}; };
currentQuery.ValueChanged += text => currentQuery.ValueChanged += text => queueUpdateSearch(!string.IsNullOrEmpty(text.NewValue));
{
queryChangedDebounce?.Cancel();
if (string.IsNullOrEmpty(text.NewValue))
Scheduler.AddOnce(updateSearch);
else
{
BeatmapSets = null;
ResultAmounts = null;
queryChangedDebounce = Scheduler.AddDelayed(updateSearch, 500);
}
};
currentQuery.BindTo(Filter.Search.Current); currentQuery.BindTo(Filter.Search.Current);
@ -170,7 +157,7 @@ namespace osu.Game.Overlays
if (Header.Tabs.Current.Value != DirectTab.Search && tab.NewValue != (DirectSortCriteria)Header.Tabs.Current.Value) if (Header.Tabs.Current.Value != DirectTab.Search && tab.NewValue != (DirectSortCriteria)Header.Tabs.Current.Value)
Header.Tabs.Current.Value = DirectTab.Search; Header.Tabs.Current.Value = DirectTab.Search;
Scheduler.AddOnce(updateSearch); queueUpdateSearch();
}; };
updateResultCounts(); updateResultCounts();
@ -242,37 +229,42 @@ namespace osu.Game.Overlays
// Queries are allowed to be run only on the first pop-in // Queries are allowed to be run only on the first pop-in
if (getSetsRequest == null) if (getSetsRequest == null)
Scheduler.AddOnce(updateSearch); queueUpdateSearch();
} }
private SearchBeatmapSetsRequest getSetsRequest; private SearchBeatmapSetsRequest getSetsRequest;
private readonly Bindable<string> currentQuery = new Bindable<string>(); private readonly Bindable<string> currentQuery = new Bindable<string>(string.Empty);
private ScheduledDelegate queryChangedDebounce; private ScheduledDelegate queryChangedDebounce;
private PreviewTrackManager previewTrackManager; private PreviewTrackManager previewTrackManager;
private void queueUpdateSearch(bool queryTextChanged = false)
{
BeatmapSets = null;
ResultAmounts = null;
getSetsRequest?.Cancel();
queryChangedDebounce?.Cancel();
queryChangedDebounce = Scheduler.AddDelayed(updateSearch, queryTextChanged ? 500 : 100);
}
private void updateSearch() private void updateSearch()
{ {
queryChangedDebounce?.Cancel();
if (!IsLoaded) if (!IsLoaded)
return; return;
if (State == Visibility.Hidden) if (State == Visibility.Hidden)
return; return;
BeatmapSets = null;
ResultAmounts = null;
getSetsRequest?.Cancel();
if (api == null) if (api == null)
return; return;
previewTrackManager.StopAnyPlaying(this); previewTrackManager.StopAnyPlaying(this);
getSetsRequest = new SearchBeatmapSetsRequest(currentQuery.Value ?? string.Empty, getSetsRequest = new SearchBeatmapSetsRequest(
currentQuery.Value,
((FilterControl)Filter).Ruleset.Value, ((FilterControl)Filter).Ruleset.Value,
Filter.DisplayStyleControl.Dropdown.Current.Value, Filter.DisplayStyleControl.Dropdown.Current.Value,
Filter.Tabs.Current.Value); //todo: sort direction (?) Filter.Tabs.Current.Value); //todo: sort direction (?)

View File

@ -22,7 +22,7 @@ namespace osu.Game.Overlays.Toolbar
public Action OnHome; public Action OnHome;
private readonly ToolbarUserArea userArea; private ToolbarUserArea userArea;
protected override bool BlockPositionalInput => false; protected override bool BlockPositionalInput => false;
@ -34,6 +34,13 @@ namespace osu.Game.Overlays.Toolbar
private readonly Bindable<OverlayActivation> overlayActivationMode = new Bindable<OverlayActivation>(OverlayActivation.All); private readonly Bindable<OverlayActivation> overlayActivationMode = new Bindable<OverlayActivation>(OverlayActivation.All);
public Toolbar() public Toolbar()
{
RelativeSizeAxes = Axes.X;
Size = new Vector2(1, HEIGHT);
}
[BackgroundDependencyLoader(true)]
private void load(OsuGame osuGame)
{ {
Children = new Drawable[] Children = new Drawable[]
{ {
@ -76,13 +83,6 @@ namespace osu.Game.Overlays.Toolbar
} }
}; };
RelativeSizeAxes = Axes.X;
Size = new Vector2(1, HEIGHT);
}
[BackgroundDependencyLoader(true)]
private void load(OsuGame osuGame)
{
StateChanged += visibility => StateChanged += visibility =>
{ {
if (overlayActivationMode.Value == OverlayActivation.Disabled) if (overlayActivationMode.Value == OverlayActivation.Disabled)

View File

@ -14,10 +14,6 @@ namespace osu.Game.Rulesets.Mods
public abstract class ModAutoplay<T> : ModAutoplay, IApplicableToRulesetContainer<T> public abstract class ModAutoplay<T> : ModAutoplay, IApplicableToRulesetContainer<T>
where T : HitObject where T : HitObject
{ {
protected virtual Score CreateReplayScore(Beatmap<T> beatmap) => new Score { Replay = new Replay() };
public override bool HasImplementation => GetType().GenericTypeArguments.Length == 0;
public virtual void ApplyToRulesetContainer(RulesetContainer<T> rulesetContainer) => rulesetContainer.SetReplayScore(CreateReplayScore(rulesetContainer.Beatmap)); public virtual void ApplyToRulesetContainer(RulesetContainer<T> rulesetContainer) => rulesetContainer.SetReplayScore(CreateReplayScore(rulesetContainer.Beatmap));
} }
@ -31,5 +27,9 @@ namespace osu.Game.Rulesets.Mods
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public bool AllowFail => false; public bool AllowFail => false;
public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModSuddenDeath), typeof(ModNoFail) }; public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModSuddenDeath), typeof(ModNoFail) };
public override bool HasImplementation => GetType().GenericTypeArguments.Length == 0;
public virtual Score CreateReplayScore(IBeatmap beatmap) => new Score { Replay = new Replay() };
} }
} }

View File

@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Mods
public override ModType Type => ModType.DifficultyIncrease; public override ModType Type => ModType.DifficultyIncrease;
public override string Description => "Zoooooooooom..."; public override string Description => "Zoooooooooom...";
public override bool Ranked => true; public override bool Ranked => true;
public override Type[] IncompatibleMods => new[] { typeof(ModHalfTime) }; public override Type[] IncompatibleMods => new[] { typeof(ModHalfTime), typeof(ModTimeRamp) };
public virtual void ApplyToClock(IAdjustableClock clock) public virtual void ApplyToClock(IAdjustableClock clock)
{ {

View File

@ -61,7 +61,7 @@ namespace osu.Game.Rulesets.Mods
public abstract class Flashlight : Drawable public abstract class Flashlight : Drawable
{ {
internal BindableInt Combo; internal BindableInt Combo;
private Shader shader; private IShader shader;
protected override DrawNode CreateDrawNode() => new FlashlightDrawNode(); protected override DrawNode CreateDrawNode() => new FlashlightDrawNode();
@ -139,7 +139,7 @@ namespace osu.Game.Rulesets.Mods
private class FlashlightDrawNode : DrawNode private class FlashlightDrawNode : DrawNode
{ {
public Shader Shader; public IShader Shader;
public Quad ScreenSpaceDrawQuad; public Quad ScreenSpaceDrawQuad;
public Vector2 FlashlightPosition; public Vector2 FlashlightPosition;
public Vector2 FlashlightSize; public Vector2 FlashlightSize;

View File

@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Mods
public override ModType Type => ModType.DifficultyReduction; public override ModType Type => ModType.DifficultyReduction;
public override string Description => "Less zoom..."; public override string Description => "Less zoom...";
public override bool Ranked => true; public override bool Ranked => true;
public override Type[] IncompatibleMods => new[] { typeof(ModDoubleTime) }; public override Type[] IncompatibleMods => new[] { typeof(ModDoubleTime), typeof(ModTimeRamp) };
public virtual void ApplyToClock(IAdjustableClock clock) public virtual void ApplyToClock(IAdjustableClock clock)
{ {

View File

@ -0,0 +1,64 @@
// 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.Linq;
using osu.Framework.Audio;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osuTK;
namespace osu.Game.Rulesets.Mods
{
public abstract class ModTimeRamp : Mod
{
public override Type[] IncompatibleMods => new[] { typeof(ModDoubleTime), typeof(ModHalfTime) };
protected abstract double FinalRateAdjustment { get; }
}
public abstract class ModTimeRamp<T> : ModTimeRamp, IUpdatableByPlayfield, IApplicableToClock, IApplicableToBeatmap<T>
where T : HitObject
{
private double finalRateTime;
private double beginRampTime;
private IAdjustableClock clock;
private IHasPitchAdjust pitchAdjust;
/// <summary>
/// The point in the beatmap at which the final ramping rate should be reached.
/// </summary>
private const double final_rate_progress = 0.75f;
public virtual void ApplyToClock(IAdjustableClock clock)
{
this.clock = clock;
pitchAdjust = (IHasPitchAdjust)clock;
// for preview purposes
pitchAdjust.PitchAdjust = 1.0 + FinalRateAdjustment;
}
public virtual void ApplyToBeatmap(Beatmap<T> beatmap)
{
HitObject lastObject = beatmap.HitObjects.LastOrDefault();
beginRampTime = beatmap.HitObjects.FirstOrDefault()?.StartTime ?? 0;
finalRateTime = final_rate_progress * ((lastObject as IHasEndTime)?.EndTime ?? lastObject?.StartTime ?? 0);
}
public virtual void Update(Playfield playfield)
{
var absRate = Math.Abs(FinalRateAdjustment);
var adjustment = MathHelper.Clamp(absRate * ((clock.CurrentTime - beginRampTime) / finalRateTime), 0, absRate);
pitchAdjust.PitchAdjust = 1 + Math.Sign(FinalRateAdjustment) * adjustment;
}
}
}

View File

@ -0,0 +1,19 @@
// 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.Game.Graphics;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Mods
{
public class ModWindDown<T> : ModTimeRamp<T>
where T : HitObject
{
public override string Name => "Wind Down";
public override string Acronym => "WD";
public override string Description => "Sloooow doooown...";
public override FontAwesome Icon => FontAwesome.fa_chevron_circle_down;
public override double ScoreMultiplier => 1.0;
protected override double FinalRateAdjustment => -0.25;
}
}

View File

@ -0,0 +1,19 @@
// 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.Game.Graphics;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Mods
{
public class ModWindUp<T> : ModTimeRamp<T>
where T : HitObject
{
public override string Name => "Wind Up";
public override string Acronym => "WU";
public override string Description => "Can you keep up?";
public override FontAwesome Icon => FontAwesome.fa_chevron_circle_up;
public override double ScoreMultiplier => 1.0;
protected override double FinalRateAdjustment => 0.5;
}
}

View File

@ -0,0 +1,148 @@
// 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 osuTK;
namespace osu.Game.Rulesets.Objects
{
public static class SliderEventGenerator
{
public static IEnumerable<SliderEventDescriptor> Generate(double startTime, double spanDuration, double velocity, double tickDistance, double totalDistance, int spanCount, double? legacyLastTickOffset)
{
// A very lenient maximum length of a slider for ticks to be generated.
// This exists for edge cases such as /b/1573664 where the beatmap has been edited by the user, and should never be reached in normal usage.
const double max_length = 100000;
var length = Math.Min(max_length, totalDistance);
tickDistance = MathHelper.Clamp(tickDistance, 0, length);
var minDistanceFromEnd = velocity * 10;
yield return new SliderEventDescriptor
{
Type = SliderEventType.Head,
SpanIndex = 0,
SpanStartTime = startTime,
Time = startTime,
PathProgress = 0,
};
if (tickDistance != 0)
{
for (var span = 0; span < spanCount; span++)
{
var spanStartTime = startTime + span * spanDuration;
var reversed = span % 2 == 1;
for (var d = tickDistance; d <= length; d += tickDistance)
{
if (d > length - minDistanceFromEnd)
break;
var pathProgress = d / length;
var timeProgress = reversed ? 1 - pathProgress : pathProgress;
yield return new SliderEventDescriptor
{
Type = SliderEventType.Tick,
SpanIndex = span,
SpanStartTime = spanStartTime,
Time = spanStartTime + timeProgress * spanDuration,
PathProgress = pathProgress,
};
}
if (span < spanCount - 1)
{
yield return new SliderEventDescriptor
{
Type = SliderEventType.Repeat,
SpanIndex = span,
SpanStartTime = startTime + span * spanDuration,
Time = spanStartTime + spanDuration,
PathProgress = (span + 1) % 2,
};
}
}
}
double totalDuration = spanCount * spanDuration;
// Okay, I'll level with you. I made a mistake. It was 2007.
// Times were simpler. osu! was but in its infancy and sliders were a new concept.
// A hack was made, which has unfortunately lived through until this day.
//
// This legacy tick is used for some calculations and judgements where audio output is not required.
// Generally we are keeping this around just for difficulty compatibility.
// Optimistically we do not want to ever use this for anything user-facing going forwards.
int finalSpanIndex = spanCount - 1;
double finalSpanStartTime = startTime + finalSpanIndex * spanDuration;
double finalSpanEndTime = Math.Max(startTime + totalDuration / 2, (finalSpanStartTime + spanDuration) - (legacyLastTickOffset ?? 0));
double finalProgress = (finalSpanEndTime - finalSpanStartTime) / spanDuration;
if (spanCount % 2 == 0) finalProgress = 1 - finalProgress;
yield return new SliderEventDescriptor
{
Type = SliderEventType.LegacyLastTick,
SpanIndex = finalSpanIndex,
SpanStartTime = finalSpanStartTime,
Time = finalSpanEndTime,
PathProgress = finalProgress,
};
yield return new SliderEventDescriptor
{
Type = SliderEventType.Tail,
SpanIndex = finalSpanIndex,
SpanStartTime = startTime + (spanCount - 1) * spanDuration,
Time = startTime + totalDuration,
PathProgress = spanCount % 2,
};
}
}
/// <summary>
/// Describes a point in time on a slider given special meaning.
/// Should be used by rulesets to visualise the slider.
/// </summary>
public struct SliderEventDescriptor
{
/// <summary>
/// The type of event.
/// </summary>
public SliderEventType Type;
/// <summary>
/// The time of this event.
/// </summary>
public double Time;
/// <summary>
/// The zero-based index of the span. In the case of repeat sliders, this will increase after each <see cref="SliderEventType.Repeat"/>.
/// </summary>
public int SpanIndex;
/// <summary>
/// The time at which the contained <see cref="SpanIndex"/> begins.
/// </summary>
public double SpanStartTime;
/// <summary>
/// The progress along the slider's <see cref="SliderPath"/> at which this event occurs.
/// </summary>
public double PathProgress;
}
public enum SliderEventType
{
Tick,
LegacyLastTick,
Head,
Tail,
Repeat
}
}

View File

@ -1,14 +1,12 @@
// 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 osu.Game.Rulesets.Objects;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Replays; using osu.Game.Replays;
namespace osu.Game.Rulesets.Replays namespace osu.Game.Rulesets.Replays
{ {
public abstract class AutoGenerator<T> : IAutoGenerator public abstract class AutoGenerator : IAutoGenerator
where T : HitObject
{ {
/// <summary> /// <summary>
/// Creates the auto replay and returns it. /// Creates the auto replay and returns it.
@ -21,11 +19,11 @@ namespace osu.Game.Rulesets.Replays
/// <summary> /// <summary>
/// The beatmap we're making. /// The beatmap we're making.
/// </summary> /// </summary>
protected Beatmap<T> Beatmap; protected IBeatmap Beatmap;
#endregion #endregion
protected AutoGenerator(Beatmap<T> beatmap) protected AutoGenerator(IBeatmap beatmap)
{ {
Beatmap = beatmap; Beatmap = beatmap;
} }

View File

@ -42,7 +42,7 @@ namespace osu.Game.Rulesets
/// <returns>An enumerable of constructed <see cref="Mod"/>s</returns> /// <returns>An enumerable of constructed <see cref="Mod"/>s</returns>
public virtual IEnumerable<Mod> ConvertLegacyMods(LegacyMods mods) => new Mod[] { }; public virtual IEnumerable<Mod> ConvertLegacyMods(LegacyMods mods) => new Mod[] { };
public Mod GetAutoplayMod() => GetAllMods().First(mod => mod is ModAutoplay); public ModAutoplay GetAutoplayMod() => GetAllMods().OfType<ModAutoplay>().First();
protected Ruleset(RulesetInfo rulesetInfo = null) protected Ruleset(RulesetInfo rulesetInfo = null)
{ {

View File

@ -10,6 +10,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osuTK; using osuTK;
@ -63,6 +64,10 @@ namespace osu.Game.Rulesets.UI
private void load(IBindable<WorkingBeatmap> beatmap) private void load(IBindable<WorkingBeatmap> beatmap)
{ {
this.beatmap = beatmap.Value; this.beatmap = beatmap.Value;
Cursor = CreateCursor();
if (Cursor != null)
CursorTargetContainer.Add(Cursor);
} }
/// <summary> /// <summary>
@ -82,6 +87,23 @@ namespace osu.Game.Rulesets.UI
/// <param name="h">The DrawableHitObject to remove.</param> /// <param name="h">The DrawableHitObject to remove.</param>
public virtual bool Remove(DrawableHitObject h) => HitObjectContainer.Remove(h); public virtual bool Remove(DrawableHitObject h) => HitObjectContainer.Remove(h);
/// <summary>
/// The cursor currently being used by this <see cref="Playfield"/>. May be null if no cursor is provided.
/// </summary>
public CursorContainer Cursor { get; private set; }
/// <summary>
/// Provide an optional cursor which is to be used for gameplay.
/// If providing a cursor, <see cref="CursorTargetContainer"/> must also point to a valid target container.
/// </summary>
/// <returns>The cursor, or null if a cursor is not rqeuired.</returns>
protected virtual CursorContainer CreateCursor() => null;
/// <summary>
/// The target container to add the cursor after it is created.
/// </summary>
protected virtual Container CursorTargetContainer => null;
/// <summary> /// <summary>
/// Registers a <see cref="Playfield"/> as a nested <see cref="Playfield"/>. /// Registers a <see cref="Playfield"/> as a nested <see cref="Playfield"/>.
/// This does not add the <see cref="Playfield"/> to the draw hierarchy. /// This does not add the <see cref="Playfield"/> to the draw hierarchy.

View File

@ -16,7 +16,9 @@ using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.Cursor;
using osu.Game.Input.Handlers; using osu.Game.Input.Handlers;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Replays; using osu.Game.Replays;
@ -32,7 +34,7 @@ namespace osu.Game.Rulesets.UI
/// Should not be derived - derive <see cref="RulesetContainer{TObject}"/> instead. /// Should not be derived - derive <see cref="RulesetContainer{TObject}"/> instead.
/// </para> /// </para>
/// </summary> /// </summary>
public abstract class RulesetContainer : Container public abstract class RulesetContainer : Container, IProvideCursor
{ {
/// <summary> /// <summary>
/// The selected variant. /// The selected variant.
@ -74,10 +76,11 @@ namespace osu.Game.Rulesets.UI
/// </summary> /// </summary>
public Container Overlays { get; protected set; } public Container Overlays { get; protected set; }
/// <summary> public CursorContainer Cursor => Playfield.Cursor;
/// The cursor provided by this <see cref="RulesetContainer"/>. May be null if no cursor is provided.
/// </summary> public bool ProvidingUserCursor => Playfield.Cursor != null && !HasReplayLoaded.Value;
public readonly CursorContainer Cursor;
protected override bool OnHover(HoverEvent e) => true; // required for IProvideCursor
public readonly Ruleset Ruleset; public readonly Ruleset Ruleset;
@ -101,8 +104,6 @@ namespace osu.Game.Rulesets.UI
KeyBindingInputManager.UseParentInput = !paused.NewValue; KeyBindingInputManager.UseParentInput = !paused.NewValue;
}; };
Cursor = CreateCursor();
} }
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
@ -259,9 +260,6 @@ namespace osu.Game.Rulesets.UI
Playfield Playfield
}); });
if (Cursor != null)
KeyBindingInputManager.Add(Cursor);
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
KeyBindingInputManager, KeyBindingInputManager,

View File

@ -89,7 +89,7 @@ namespace osu.Game.Screens
/// </summary> /// </summary>
public class ShaderPrecompiler : Drawable public class ShaderPrecompiler : Drawable
{ {
private readonly List<Shader> loadTargets = new List<Shader>(); private readonly List<IShader> loadTargets = new List<IShader>();
public bool FinishedCompiling { get; private set; } public bool FinishedCompiling { get; private set; }
@ -106,7 +106,7 @@ namespace osu.Game.Screens
loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE)); loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE));
} }
protected virtual bool AllLoaded => loadTargets.All(s => s.Loaded); protected virtual bool AllLoaded => loadTargets.All(s => s.IsLoaded);
protected override void Update() protected override void Update()
{ {

View File

@ -63,7 +63,7 @@ namespace osu.Game.Screens.Menu
private readonly float[] frequencyAmplitudes = new float[256]; private readonly float[] frequencyAmplitudes = new float[256];
private Shader shader; private IShader shader;
private readonly Texture texture; private readonly Texture texture;
public LogoVisualisation() public LogoVisualisation()
@ -131,8 +131,6 @@ namespace osu.Game.Screens.Menu
protected override DrawNode CreateDrawNode() => new VisualisationDrawNode(); protected override DrawNode CreateDrawNode() => new VisualisationDrawNode();
private readonly VisualiserSharedData sharedData = new VisualiserSharedData();
protected override void ApplyDrawNode(DrawNode node) protected override void ApplyDrawNode(DrawNode node)
{ {
base.ApplyDrawNode(node); base.ApplyDrawNode(node);
@ -142,29 +140,23 @@ namespace osu.Game.Screens.Menu
visNode.Shader = shader; visNode.Shader = shader;
visNode.Texture = texture; visNode.Texture = texture;
visNode.Size = DrawSize.X; visNode.Size = DrawSize.X;
visNode.Shared = sharedData;
visNode.Colour = AccentColour; visNode.Colour = AccentColour;
visNode.AudioData = frequencyAmplitudes; visNode.AudioData = frequencyAmplitudes;
} }
private class VisualiserSharedData
{
public readonly QuadBatch<TexturedVertex2D> VertexBatch = new QuadBatch<TexturedVertex2D>(100, 10);
}
private class VisualisationDrawNode : DrawNode private class VisualisationDrawNode : DrawNode
{ {
public Shader Shader; public IShader Shader;
public Texture Texture; public Texture Texture;
public VisualiserSharedData Shared;
//Asuming the logo is a circle, we don't need a second dimension. //Asuming the logo is a circle, we don't need a second dimension.
public float Size; public float Size;
public Color4 Colour; public Color4 Colour;
public float[] AudioData; public float[] AudioData;
private readonly QuadBatch<TexturedVertex2D> vertexBatch = new QuadBatch<TexturedVertex2D>(100, 10);
public override void Draw(Action<TexturedVertex2D> vertexAction) public override void Draw(Action<TexturedVertex2D> vertexAction)
{ {
base.Draw(vertexAction); base.Draw(vertexAction);
@ -209,7 +201,7 @@ namespace osu.Game.Screens.Menu
rectangle, rectangle,
colourInfo, colourInfo,
null, null,
Shared.VertexBatch.AddAction, vertexBatch.AddAction,
//barSize by itself will make it smooth more in the X axis than in the Y axis, this reverts that. //barSize by itself will make it smooth more in the X axis than in the Y axis, this reverts that.
Vector2.Divide(inflation, barSize.Yx)); Vector2.Divide(inflation, barSize.Yx));
} }
@ -218,6 +210,13 @@ namespace osu.Game.Screens.Menu
Shader.Unbind(); Shader.Unbind();
} }
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
vertexBatch.Dispose();
}
} }
} }
} }

View File

@ -107,6 +107,14 @@ namespace osu.Game.Screens.Multi.Lounge
Filter.Search.HoldFocus = false; Filter.Search.HoldFocus = false;
} }
public override void OnResuming(IScreen last)
{
base.OnResuming(last);
if (currentRoom.Value?.RoomID.Value == null)
currentRoom.Value = new Room();
}
private void joinRequested(Room room) private void joinRequested(Room room)
{ {
processingOverlay.Show(); processingOverlay.Show();

View File

@ -2,6 +2,7 @@
// 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.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
@ -13,6 +14,7 @@ using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Overlays.SearchableList; using osu.Game.Overlays.SearchableList;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Multi.Components; using osu.Game.Screens.Multi.Components;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
using osuTK; using osuTK;
@ -108,7 +110,7 @@ namespace osu.Game.Screens.Multi.Match.Components
}, },
}; };
CurrentItem.BindValueChanged(item => modDisplay.Current.Value = item.NewValue?.RequiredMods, true); CurrentItem.BindValueChanged(item => modDisplay.Current.Value = item.NewValue?.RequiredMods ?? Enumerable.Empty<Mod>(), true);
beatmapButton.Action = () => RequestBeatmapSelection?.Invoke(); beatmapButton.Action = () => RequestBeatmapSelection?.Invoke();
} }

View File

@ -28,13 +28,15 @@ namespace osu.Game.Screens.Multi.Match.Components
{ {
base.LoadComplete(); base.LoadComplete();
roomId.BindValueChanged(_ => updateChannel(), true); channelId.BindValueChanged(_ => updateChannel(), true);
} }
private void updateChannel() private void updateChannel()
{ {
if (roomId.Value != null) if (roomId.Value == null || channelId.Value == 0)
Channel.Value = channelManager?.JoinChannel(new Channel { Id = channelId.Value, Type = ChannelType.Multiplayer, Name = $"#mp_{roomId.Value}" }); return;
Channel.Value = channelManager?.JoinChannel(new Channel { Id = channelId.Value, Type = ChannelType.Multiplayer, Name = $"#lazermp_{roomId.Value}" });
} }
} }
} }

View File

@ -0,0 +1,151 @@
// 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.Linq;
using System.Threading.Tasks;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Screens.Play
{
/// <summary>
/// Encapsulates gameplay timing logic and provides a <see cref="GameplayClock"/> for children.
/// </summary>
public class GameplayClockContainer : Container
{
private readonly WorkingBeatmap beatmap;
/// <summary>
/// The original source (usually a <see cref="WorkingBeatmap"/>'s track).
/// </summary>
private readonly IAdjustableClock sourceClock;
public readonly BindableBool IsPaused = new BindableBool();
/// <summary>
/// The decoupled clock used for gameplay. Should be used for seeks and clock control.
/// </summary>
private readonly DecoupleableInterpolatingFramedClock adjustableClock;
public readonly Bindable<double> UserPlaybackRate = new BindableDouble(1)
{
Default = 1,
MinValue = 0.5,
MaxValue = 2,
Precision = 0.1,
};
/// <summary>
/// The final clock which is exposed to underlying components.
/// </summary>
[Cached]
private readonly GameplayClock gameplayClock;
private Bindable<double> userAudioOffset;
private readonly FramedOffsetClock offsetClock;
public GameplayClockContainer(WorkingBeatmap beatmap, bool allowLeadIn, double gameplayStartTime)
{
this.beatmap = beatmap;
RelativeSizeAxes = Axes.Both;
sourceClock = (IAdjustableClock)beatmap.Track ?? new StopwatchClock();
adjustableClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false };
adjustableClock.Seek(allowLeadIn
? Math.Min(0, gameplayStartTime - beatmap.BeatmapInfo.AudioLeadIn)
: gameplayStartTime);
adjustableClock.ProcessFrame();
// Lazer's audio timings in general doesn't match stable. This is the result of user testing, albeit limited.
// This only seems to be required on windows. We need to eventually figure out why, with a bit of luck.
var platformOffsetClock = new FramedOffsetClock(adjustableClock) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 22 : 0 };
// the final usable gameplay clock with user-set offsets applied.
offsetClock = new FramedOffsetClock(platformOffsetClock);
// the clock to be exposed via DI to children.
gameplayClock = new GameplayClock(offsetClock);
}
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
userAudioOffset = config.GetBindable<double>(OsuSetting.AudioOffset);
userAudioOffset.BindValueChanged(offset => offsetClock.Offset = offset.NewValue, true);
UserPlaybackRate.ValueChanged += _ => updateRate();
}
public void Restart()
{
Task.Run(() =>
{
sourceClock.Reset();
Schedule(() =>
{
adjustableClock.ChangeSource(sourceClock);
updateRate();
this.Delay(750).Schedule(() =>
{
if (!IsPaused.Value)
{
adjustableClock.Start();
}
});
});
});
}
public void Start()
{
// Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time
// This accounts for the audio clock source potentially taking time to enter a completely stopped state
adjustableClock.Seek(adjustableClock.CurrentTime);
adjustableClock.Start();
}
public void Seek(double time) => adjustableClock.Seek(time);
public void Stop() => adjustableClock.Stop();
public void ResetLocalAdjustments()
{
// In the case of replays, we may have changed the playback rate.
UserPlaybackRate.Value = 1;
}
protected override void Update()
{
if (!IsPaused.Value)
offsetClock.ProcessFrame();
base.Update();
}
private void updateRate()
{
if (sourceClock == null) return;
sourceClock.Rate = 1;
foreach (var mod in beatmap.Mods.Value.OfType<IApplicableToClock>())
mod.ApplyToClock(sourceClock);
sourceClock.Rate *= UserPlaybackRate.Value;
}
}
}

View File

@ -92,30 +92,6 @@ namespace osu.Game.Screens.Play.HUD
public Action HoverGained; public Action HoverGained;
public Action HoverLost; public Action HoverLost;
public bool OnPressed(GlobalAction action)
{
switch (action)
{
case GlobalAction.Back:
BeginConfirm();
return true;
}
return false;
}
public bool OnReleased(GlobalAction action)
{
switch (action)
{
case GlobalAction.Back:
AbortConfirm();
return true;
}
return false;
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours)
{ {
@ -178,7 +154,7 @@ namespace osu.Game.Screens.Play.HUD
// avoid starting a new confirm call until we finish animating. // avoid starting a new confirm call until we finish animating.
pendingAnimation = true; pendingAnimation = true;
Progress.Value = 0; AbortConfirm();
overlayCircle.ScaleTo(0, 100) overlayCircle.ScaleTo(0, 100)
.Then().FadeOut().ScaleTo(1).FadeIn(500) .Then().FadeOut().ScaleTo(1).FadeIn(500)
@ -207,6 +183,31 @@ namespace osu.Game.Screens.Play.HUD
base.OnHoverLost(e); base.OnHoverLost(e);
} }
public bool OnPressed(GlobalAction action)
{
switch (action)
{
case GlobalAction.Back:
if (!pendingAnimation)
BeginConfirm();
return true;
}
return false;
}
public bool OnReleased(GlobalAction action)
{
switch (action)
{
case GlobalAction.Back:
AbortConfirm();
return true;
}
return false;
}
protected override bool OnMouseDown(MouseDownEvent e) protected override bool OnMouseDown(MouseDownEvent e)
{ {
if (!pendingAnimation && e.CurrentState.Mouse.Buttons.Count() == 1) if (!pendingAnimation && e.CurrentState.Mouse.Buttons.Count() == 1)

View File

@ -19,7 +19,9 @@ namespace osu.Game.Screens.Play.HUD
public readonly PlaybackSettings PlaybackSettings; public readonly PlaybackSettings PlaybackSettings;
public readonly VisualSettings VisualSettings; public readonly VisualSettings VisualSettings;
//public readonly CollectionSettings CollectionSettings; //public readonly CollectionSettings CollectionSettings;
//public readonly DiscussionSettings DiscussionSettings; //public readonly DiscussionSettings DiscussionSettings;
public PlayerSettingsOverlay() public PlayerSettingsOverlay()

View File

@ -1,12 +1,12 @@
// 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 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.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Timing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
@ -40,7 +40,9 @@ namespace osu.Game.Screens.Play
private static bool hasShownNotificationOnce; private static bool hasShownNotificationOnce;
public HUDOverlay(ScoreProcessor scoreProcessor, RulesetContainer rulesetContainer, WorkingBeatmap working, IAdjustableClock adjustableClock) public Action<double> RequestSeek;
public HUDOverlay(ScoreProcessor scoreProcessor, RulesetContainer rulesetContainer, WorkingBeatmap working)
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
@ -92,11 +94,9 @@ namespace osu.Game.Screens.Play
Progress.Objects = rulesetContainer.Objects; Progress.Objects = rulesetContainer.Objects;
Progress.AllowSeeking = rulesetContainer.HasReplayLoaded.Value; Progress.AllowSeeking = rulesetContainer.HasReplayLoaded.Value;
Progress.RequestSeek = pos => adjustableClock.Seek(pos); Progress.RequestSeek = time => RequestSeek(time);
ModDisplay.Current.BindTo(working.Mods); ModDisplay.Current.BindTo(working.Mods);
PlayerSettingsOverlay.PlaybackSettings.AdjustableClock = adjustableClock;
} }
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]

View File

@ -7,16 +7,13 @@ 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.Timing;
using osu.Game.Graphics; using osu.Game.Graphics;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Screens.Play namespace osu.Game.Screens.Play
{ {
/// <summary> /// <summary>
/// A container which handles pausing children, displaying a pause overlay with choices and processing the clock. /// A container which handles pausing children, displaying an overlay blocking its children during paused state.
/// Exposes a <see cref="GameplayClock"/> to children via DI.
/// This alleviates a lot of the intricate pause logic from being in <see cref="Player"/>
/// </summary> /// </summary>
public class PausableGameplayContainer : Container public class PausableGameplayContainer : Container
{ {
@ -44,46 +41,33 @@ namespace osu.Game.Screens.Play
public Action OnRetry; public Action OnRetry;
public Action OnQuit; public Action OnQuit;
private readonly FramedClock offsetClock; public Action Stop;
private readonly DecoupleableInterpolatingFramedClock adjustableClock; public Action Start;
/// <summary>
/// The final clock which is exposed to underlying components.
/// </summary>
[Cached]
private readonly GameplayClock gameplayClock;
/// <summary> /// <summary>
/// Creates a new <see cref="PausableGameplayContainer"/>. /// Creates a new <see cref="PausableGameplayContainer"/>.
/// </summary> /// </summary>
/// <param name="offsetClock">The gameplay clock. This is the clock that will process frames. Includes user/system offsets.</param> public PausableGameplayContainer()
/// <param name="adjustableClock">The seekable clock. This is the clock that will be paused and resumed. Should not be processed (it is processed automatically by <see cref="offsetClock"/>).</param>
public PausableGameplayContainer(FramedClock offsetClock, DecoupleableInterpolatingFramedClock adjustableClock)
{ {
this.offsetClock = offsetClock;
this.adjustableClock = adjustableClock;
gameplayClock = new GameplayClock(offsetClock);
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
AddInternal(content = new Container InternalChildren = new[]
{ {
Clock = this.offsetClock, content = new Container
ProcessCustomClock = false,
RelativeSizeAxes = Axes.Both
});
AddInternal(pauseOverlay = new PauseOverlay
{
OnResume = () =>
{ {
IsResuming = true; RelativeSizeAxes = Axes.Both
this.Delay(400).Schedule(Resume);
}, },
OnRetry = () => OnRetry(), pauseOverlay = new PauseOverlay
OnQuit = () => OnQuit(), {
}); OnResume = () =>
{
IsResuming = true;
this.Delay(400).Schedule(Resume);
},
OnRetry = () => OnRetry(),
OnQuit = () => OnQuit(),
}
};
} }
public void Pause(bool force = false) => Schedule(() => // Scheduled to ensure a stable position in execution order, no matter how it was called. public void Pause(bool force = false) => Schedule(() => // Scheduled to ensure a stable position in execution order, no matter how it was called.
@ -93,7 +77,7 @@ namespace osu.Game.Screens.Play
if (IsPaused.Value) return; if (IsPaused.Value) return;
// stop the seekable clock (stops the audio eventually) // stop the seekable clock (stops the audio eventually)
adjustableClock.Stop(); Stop?.Invoke();
IsPaused.Value = true; IsPaused.Value = true;
pauseOverlay.Show(); pauseOverlay.Show();
@ -105,14 +89,12 @@ namespace osu.Game.Screens.Play
{ {
if (!IsPaused.Value) return; if (!IsPaused.Value) return;
IsPaused.Value = false;
IsResuming = false; IsResuming = false;
lastPauseActionTime = Time.Current; lastPauseActionTime = Time.Current;
// Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time IsPaused.Value = false;
// This accounts for the audio clock source potentially taking time to enter a completely stopped state
adjustableClock.Seek(adjustableClock.CurrentTime); Start?.Invoke();
adjustableClock.Start();
pauseOverlay.Hide(); pauseOverlay.Hide();
} }
@ -131,9 +113,6 @@ namespace osu.Game.Screens.Play
if (!game.IsActive.Value && CanPause) if (!game.IsActive.Value && CanPause)
Pause(); Pause();
if (!IsPaused.Value)
offsetClock.ProcessFrame();
base.Update(); base.Update();
} }

View File

@ -3,24 +3,19 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using osu.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
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.Cursor;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Framework.Timing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Rulesets; using osu.Game.Rulesets;
@ -34,7 +29,7 @@ using osu.Game.Storyboards.Drawables;
namespace osu.Game.Screens.Play namespace osu.Game.Screens.Play
{ {
public class Player : ScreenWithBeatmapBackground, IProvideCursor public class Player : ScreenWithBeatmapBackground
{ {
protected override bool AllowBackButton => false; // handled by HoldForMenuButton protected override bool AllowBackButton => false; // handled by HoldForMenuButton
@ -53,22 +48,11 @@ namespace osu.Game.Screens.Play
public bool AllowResults { get; set; } = true; public bool AllowResults { get; set; } = true;
private Bindable<bool> mouseWheelDisabled; private Bindable<bool> mouseWheelDisabled;
private Bindable<double> userAudioOffset;
private readonly Bindable<bool> storyboardReplacesBackground = new Bindable<bool>(); private readonly Bindable<bool> storyboardReplacesBackground = new Bindable<bool>();
public int RestartCount; public int RestartCount;
public CursorContainer Cursor => RulesetContainer.Cursor;
public bool ProvidingUserCursor => RulesetContainer?.Cursor != null && !RulesetContainer.HasReplayLoaded.Value;
private IAdjustableClock sourceClock;
/// <summary>
/// The decoupled clock used for gameplay. Should be used for seeks and clock control.
/// </summary>
private DecoupleableInterpolatingFramedClock adjustableClock;
[Resolved] [Resolved]
private ScoreManager scoreManager { get; set; } private ScoreManager scoreManager { get; set; }
@ -98,25 +82,113 @@ namespace osu.Game.Screens.Play
public bool LoadedBeatmapSuccessfully => RulesetContainer?.Objects.Any() == true; public bool LoadedBeatmapSuccessfully => RulesetContainer?.Objects.Any() == true;
private GameplayClockContainer gameplayClockContainer;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(AudioManager audio, APIAccess api, OsuConfigManager config) private void load(AudioManager audio, APIAccess api, OsuConfigManager config)
{ {
this.api = api; this.api = api;
WorkingBeatmap working = Beatmap.Value; WorkingBeatmap working = loadBeatmap();
if (working is DummyWorkingBeatmap)
if (working == null)
return; return;
sampleRestart = audio.Sample.Get(@"Gameplay/restart"); sampleRestart = audio.Sample.Get(@"Gameplay/restart");
mouseWheelDisabled = config.GetBindable<bool>(OsuSetting.MouseDisableWheel); mouseWheelDisabled = config.GetBindable<bool>(OsuSetting.MouseDisableWheel);
userAudioOffset = config.GetBindable<double>(OsuSetting.AudioOffset);
IBeatmap beatmap; ScoreProcessor = RulesetContainer.CreateScoreProcessor();
if (!ScoreProcessor.Mode.Disabled)
config.BindWith(OsuSetting.ScoreDisplayMode, ScoreProcessor.Mode);
InternalChild = gameplayClockContainer = new GameplayClockContainer(working, AllowLeadIn, RulesetContainer.GameplayStartTime);
gameplayClockContainer.Children = new Drawable[]
{
PausableGameplayContainer = new PausableGameplayContainer
{
Retries = RestartCount,
OnRetry = restart,
OnQuit = performUserRequestedExit,
Start = gameplayClockContainer.Start,
Stop = gameplayClockContainer.Stop,
IsPaused = { BindTarget = gameplayClockContainer.IsPaused },
CheckCanPause = () => AllowPause && ValidForResume && !HasFailed && !RulesetContainer.HasReplayLoaded.Value,
Children = new[]
{
StoryboardContainer = CreateStoryboardContainer(),
new ScalingContainer(ScalingMode.Gameplay)
{
Child = new LocalSkinOverrideContainer(working.Skin)
{
RelativeSizeAxes = Axes.Both,
Child = RulesetContainer
}
},
new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Breaks = working.Beatmap.Breaks
},
// display the cursor above some HUD elements.
RulesetContainer.Cursor?.CreateProxy() ?? new Container(),
HUDOverlay = new HUDOverlay(ScoreProcessor, RulesetContainer, working)
{
HoldToQuit = { Action = performUserRequestedExit },
PlayerSettingsOverlay = { PlaybackSettings = { UserPlaybackRate = { BindTarget = gameplayClockContainer.UserPlaybackRate } } },
KeyCounter = { Visible = { BindTarget = RulesetContainer.HasReplayLoaded } },
RequestSeek = gameplayClockContainer.Seek,
Anchor = Anchor.Centre,
Origin = Anchor.Centre
},
new SkipOverlay(RulesetContainer.GameplayStartTime)
{
RequestSeek = gameplayClockContainer.Seek
},
}
},
failOverlay = new FailOverlay
{
OnRetry = restart,
OnQuit = performUserRequestedExit,
},
new HotkeyRetryOverlay
{
Action = () =>
{
if (!this.IsCurrentScreen()) return;
fadeOut(true);
restart();
},
}
};
// bind clock into components that require it
RulesetContainer.IsPaused.BindTo(gameplayClockContainer.IsPaused);
if (ShowStoryboard.Value)
initializeStoryboard(false);
// Bind ScoreProcessor to ourselves
ScoreProcessor.AllJudged += onCompletion;
ScoreProcessor.Failed += onFail;
foreach (var mod in Beatmap.Value.Mods.Value.OfType<IApplicableToScoreProcessor>())
mod.ApplyToScoreProcessor(ScoreProcessor);
}
private WorkingBeatmap loadBeatmap()
{
WorkingBeatmap working = Beatmap.Value;
if (working is DummyWorkingBeatmap)
return null;
try try
{ {
beatmap = working.Beatmap; var beatmap = working.Beatmap;
if (beatmap == null) if (beatmap == null)
throw new InvalidOperationException("Beatmap was not loaded"); throw new InvalidOperationException("Beatmap was not loaded");
@ -140,119 +212,17 @@ namespace osu.Game.Screens.Play
if (!RulesetContainer.Objects.Any()) if (!RulesetContainer.Objects.Any())
{ {
Logger.Log("Beatmap contains no hit objects!", level: LogLevel.Error); Logger.Log("Beatmap contains no hit objects!", level: LogLevel.Error);
return; return null;
} }
} }
catch (Exception e) catch (Exception e)
{ {
Logger.Error(e, "Could not load beatmap sucessfully!"); Logger.Error(e, "Could not load beatmap sucessfully!");
//couldn't load, hard abort! //couldn't load, hard abort!
return; return null;
} }
sourceClock = (IAdjustableClock)working.Track ?? new StopwatchClock(); return working;
adjustableClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false };
adjustableClock.Seek(AllowLeadIn
? Math.Min(0, RulesetContainer.GameplayStartTime - beatmap.BeatmapInfo.AudioLeadIn)
: RulesetContainer.GameplayStartTime);
adjustableClock.ProcessFrame();
// Lazer's audio timings in general doesn't match stable. This is the result of user testing, albeit limited.
// This only seems to be required on windows. We need to eventually figure out why, with a bit of luck.
var platformOffsetClock = new FramedOffsetClock(adjustableClock) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 22 : 0 };
// the final usable gameplay clock with user-set offsets applied.
var offsetClock = new FramedOffsetClock(platformOffsetClock);
userAudioOffset.ValueChanged += offset => offsetClock.Offset = offset.NewValue;
userAudioOffset.TriggerChange();
ScoreProcessor = RulesetContainer.CreateScoreProcessor();
if (!ScoreProcessor.Mode.Disabled)
config.BindWith(OsuSetting.ScoreDisplayMode, ScoreProcessor.Mode);
InternalChildren = new Drawable[]
{
PausableGameplayContainer = new PausableGameplayContainer(offsetClock, adjustableClock)
{
Retries = RestartCount,
OnRetry = restart,
OnQuit = performUserRequestedExit,
CheckCanPause = () => AllowPause && ValidForResume && !HasFailed && !RulesetContainer.HasReplayLoaded.Value,
Children = new Container[]
{
StoryboardContainer = CreateStoryboardContainer(),
new ScalingContainer(ScalingMode.Gameplay)
{
Child = new LocalSkinOverrideContainer(working.Skin)
{
RelativeSizeAxes = Axes.Both,
Child = RulesetContainer
}
},
new BreakOverlay(beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Breaks = beatmap.Breaks
},
new ScalingContainer(ScalingMode.Gameplay)
{
Child = RulesetContainer.Cursor?.CreateProxy() ?? new Container(),
},
HUDOverlay = new HUDOverlay(ScoreProcessor, RulesetContainer, working, adjustableClock)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
},
new SkipOverlay(RulesetContainer.GameplayStartTime)
{
RequestSeek = time => adjustableClock.Seek(time)
},
}
},
failOverlay = new FailOverlay
{
OnRetry = restart,
OnQuit = performUserRequestedExit,
},
new HotkeyRetryOverlay
{
Action = () =>
{
if (!this.IsCurrentScreen()) return;
fadeOut(true);
restart();
},
}
};
HUDOverlay.HoldToQuit.Action = performUserRequestedExit;
HUDOverlay.KeyCounter.Visible.BindTo(RulesetContainer.HasReplayLoaded);
RulesetContainer.IsPaused.BindTo(PausableGameplayContainer.IsPaused);
if (ShowStoryboard.Value)
initializeStoryboard(false);
// Bind ScoreProcessor to ourselves
ScoreProcessor.AllJudged += onCompletion;
ScoreProcessor.Failed += onFail;
foreach (var mod in Beatmap.Value.Mods.Value.OfType<IApplicableToScoreProcessor>())
mod.ApplyToScoreProcessor(ScoreProcessor);
}
private void applyRateFromMods()
{
if (sourceClock == null) return;
sourceClock.Rate = 1;
foreach (var mod in Beatmap.Value.Mods.Value.OfType<IApplicableToClock>())
mod.ApplyToClock(sourceClock);
} }
private void performUserRequestedExit() private void performUserRequestedExit()
@ -321,7 +291,7 @@ namespace osu.Game.Screens.Play
if (Beatmap.Value.Mods.Value.OfType<IApplicableFailOverride>().Any(m => !m.AllowFail)) if (Beatmap.Value.Mods.Value.OfType<IApplicableFailOverride>().Any(m => !m.AllowFail))
return false; return false;
adjustableClock.Stop(); gameplayClockContainer.Stop();
HasFailed = true; HasFailed = true;
failOverlay.Retries = RestartCount; failOverlay.Retries = RestartCount;
@ -355,24 +325,7 @@ namespace osu.Game.Screens.Play
storyboardReplacesBackground.Value = Beatmap.Value.Storyboard.ReplacesBackground && Beatmap.Value.Storyboard.HasDrawable; storyboardReplacesBackground.Value = Beatmap.Value.Storyboard.ReplacesBackground && Beatmap.Value.Storyboard.HasDrawable;
Task.Run(() => gameplayClockContainer.Restart();
{
sourceClock.Reset();
Schedule(() =>
{
adjustableClock.ChangeSource(sourceClock);
applyRateFromMods();
this.Delay(750).Schedule(() =>
{
if (!PausableGameplayContainer.IsPaused.Value)
{
adjustableClock.Start();
}
});
});
});
PausableGameplayContainer.Alpha = 0; PausableGameplayContainer.Alpha = 0;
PausableGameplayContainer.FadeIn(750, Easing.OutQuint); PausableGameplayContainer.FadeIn(750, Easing.OutQuint);
@ -395,8 +348,8 @@ namespace osu.Game.Screens.Play
if ((!AllowPause || HasFailed || !ValidForResume || PausableGameplayContainer?.IsPaused.Value != false || RulesetContainer?.HasReplayLoaded.Value != false) && (!PausableGameplayContainer?.IsResuming ?? true)) if ((!AllowPause || HasFailed || !ValidForResume || PausableGameplayContainer?.IsPaused.Value != false || RulesetContainer?.HasReplayLoaded.Value != false) && (!PausableGameplayContainer?.IsResuming ?? true))
{ {
// In the case of replays, we may have changed the playback rate. gameplayClockContainer.ResetLocalAdjustments();
applyRateFromMods();
fadeOut(); fadeOut();
return base.OnExiting(next); return base.OnExiting(next);
} }

View File

@ -4,7 +4,6 @@
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.Timing;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
@ -16,7 +15,13 @@ namespace osu.Game.Screens.Play.PlayerSettings
protected override string Title => @"playback"; protected override string Title => @"playback";
public IAdjustableClock AdjustableClock { set; get; } public readonly Bindable<double> UserPlaybackRate = new BindableDouble(1)
{
Default = 1,
MinValue = 0.5,
MaxValue = 2,
Precision = 0.1,
};
private readonly PlayerSliderBar<double> rateSlider; private readonly PlayerSliderBar<double> rateSlider;
@ -47,31 +52,13 @@ namespace osu.Game.Screens.Play.PlayerSettings
} }
}, },
}, },
rateSlider = new PlayerSliderBar<double> rateSlider = new PlayerSliderBar<double> { Bindable = UserPlaybackRate }
{
Bindable = new BindableDouble(1)
{
Default = 1,
MinValue = 0.5,
MaxValue = 2,
Precision = 0.1,
},
}
}; };
} }
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
if (AdjustableClock == null)
return;
var clockRate = AdjustableClock.Rate;
// can't trigger this line instantly as the underlying clock may not be ready to accept adjustments yet.
rateSlider.Bindable.ValueChanged += multiplier => AdjustableClock.Rate = clockRate * multiplier.NewValue;
rateSlider.Bindable.BindValueChanged(multiplier => multiplierText.Text = $"{multiplier.NewValue:0.0}x", true); rateSlider.Bindable.BindValueChanged(multiplier => multiplierText.Text = $"{multiplier.NewValue:0.0}x", true);
} }
} }

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Humanizer; using Humanizer;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -60,9 +61,12 @@ namespace osu.Game.Screens.Select
if (base.OnExiting(next)) if (base.OnExiting(next))
return true; return true;
Beatmap.Value = beatmaps.GetWorkingBeatmap(CurrentItem.Value?.Beatmap); if (CurrentItem.Value != null)
Beatmap.Value.Mods.Value = selectedMods.Value = CurrentItem.Value?.RequiredMods; {
Ruleset.Value = CurrentItem.Value?.Ruleset; Ruleset.Value = CurrentItem.Value.Ruleset;
Beatmap.Value = beatmaps.GetWorkingBeatmap(CurrentItem.Value.Beatmap);
Beatmap.Value.Mods.Value = selectedMods.Value = CurrentItem.Value.RequiredMods ?? Enumerable.Empty<Mod>();
}
Beatmap.Disabled = true; Beatmap.Disabled = true;
Ruleset.Disabled = true; Ruleset.Disabled = true;

View File

@ -20,14 +20,14 @@ namespace osu.Game.Utils
private readonly List<Task> tasks = new List<Task>(); private readonly List<Task> tasks = new List<Task>();
private Exception lastException;
public RavenLogger(OsuGame game) public RavenLogger(OsuGame game)
{ {
raven.Release = game.Version; raven.Release = game.Version;
if (!game.IsDeployedBuild) return; if (!game.IsDeployedBuild) return;
Exception lastException = null;
Logger.NewEntry += entry => Logger.NewEntry += entry =>
{ {
if (entry.Level < LogLevel.Verbose) return; if (entry.Level < LogLevel.Verbose) return;

View File

@ -16,7 +16,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.1" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2019.128.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2019.128.0" />
<PackageReference Include="ppy.osu.Framework" Version="2019.301.0" /> <PackageReference Include="ppy.osu.Framework" Version="2019.308.0" />
<PackageReference Include="SharpCompress" Version="0.22.0" /> <PackageReference Include="SharpCompress" Version="0.22.0" />
<PackageReference Include="NUnit" Version="3.11.0" /> <PackageReference Include="NUnit" Version="3.11.0" />
<PackageReference Include="SharpRaven" Version="2.4.0" /> <PackageReference Include="SharpRaven" Version="2.4.0" />

View File

@ -105,12 +105,12 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.1" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2019.128.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2019.128.0" />
<PackageReference Include="ppy.osu.Framework" Version="2019.301.0" /> <PackageReference Include="ppy.osu.Framework" Version="2019.308.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2019.301.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2019.308.0" />
<PackageReference Include="SharpCompress" Version="0.22.0" /> <PackageReference Include="SharpCompress" Version="0.22.0" />
<PackageReference Include="NUnit" Version="3.11.0" /> <PackageReference Include="NUnit" Version="3.11.0" />
<PackageReference Include="SharpRaven" Version="2.4.0" /> <PackageReference Include="SharpRaven" Version="2.4.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="4.5.0" /> <PackageReference Include="System.ComponentModel.Annotations" Version="4.5.0" />
<PackageReference Include="ppy.osu.Framework.NativeLibs" Version="2019.208.0" ExcludeAssets="all" /> <PackageReference Include="ppy.osu.Framework.NativeLibs" Version="2019.307.0" ExcludeAssets="all" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -262,6 +262,7 @@
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=IL/@EntryIndexedValue">IL</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=IL/@EntryIndexedValue">IL</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=IP/@EntryIndexedValue">IP</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=IP/@EntryIndexedValue">IP</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=IPC/@EntryIndexedValue">IPC</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=IPC/@EntryIndexedValue">IPC</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=JIT/@EntryIndexedValue">JIT</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LTRB/@EntryIndexedValue">LTRB</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LTRB/@EntryIndexedValue">LTRB</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=MD/@EntryIndexedValue">MD5</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=MD/@EntryIndexedValue">MD5</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=NS/@EntryIndexedValue">NS</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=NS/@EntryIndexedValue">NS</s:String>