1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-06 06:17:23 +08:00

Merge branch 'master' into mania-judgemetns

This commit is contained in:
Dean Herbert 2019-03-13 18:17:42 +09:00 committed by GitHub
commit 1617e2a729
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
61 changed files with 1047 additions and 597 deletions

View File

@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Catch.Tests
{
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)
=> base.Test(expected, name);

View File

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

View File

@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{
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!" } },
Replay = new CatchAutoGenerator(beatmap).Generate(),

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Audio;
@ -25,6 +24,11 @@ namespace osu.Game.Rulesets.Catch.Objects
public double Velocity;
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)
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
@ -41,19 +45,6 @@ namespace osu.Game.Rulesets.Catch.Objects
protected override void 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
{
@ -62,81 +53,59 @@ namespace osu.Game.Rulesets.Catch.Objects
Volume = s.Volume
}).ToList();
AddNested(new Fruit
SliderEventDescriptor? lastEvent = null;
foreach (var e in SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset))
{
Samples = Samples,
StartTime = StartTime,
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)
// generate tiny droplets since the last point
if (lastEvent != null)
{
bool isLastTick = false;
if (d + minDistanceFromEnd >= length)
double sinceLastTick = e.Time - lastEvent.Value.Time;
if (sinceLastTick > 80)
{
d = length;
isLastTick = true;
}
double timeBetweenTiny = sinceLastTick;
while (timeBetweenTiny > 100)
timeBetweenTiny /= 2;
var timeProgress = d / length;
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
for (double t = timeBetweenTiny; t < sinceLastTick; t += timeBetweenTiny)
{
StartTime = t,
X = X + Path.PositionAt(progress).X / CatchPlayfield.BASE_WIDTH,
Samples = tickSamples
});
AddNested(new TinyDroplet
{
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,
StartTime = spanStartTime + spanDuration,
X = X + Path.PositionAt(reversed ? 0 : 1).X / CatchPlayfield.BASE_WIDTH
});
case SliderEventType.Tick:
AddNested(new Droplet
{
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.Game.Beatmaps;
using osu.Game.Replays;
using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Replays;
namespace osu.Game.Rulesets.Catch.Replays
{
internal class CatchAutoGenerator : AutoGenerator<CatchHitObject>
internal class CatchAutoGenerator : AutoGenerator
{
public const double RELEASE_DELAY = 20;
public CatchAutoGenerator(Beatmap<CatchHitObject> beatmap)
public new CatchBeatmap Beatmap => (CatchBeatmap)base.Beatmap;
public CatchAutoGenerator(IBeatmap beatmap)
: base(beatmap)
{
Replay = new Replay();

View File

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

View File

@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Mania.Mods
{
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!" } },
Replay = new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(),

View File

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

View File

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

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics.Cursor;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Osu.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();
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" } },
Replay = new OsuAutoGenerator(beatmap).Generate()

View File

@ -1,7 +1,6 @@
// 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 osuTK;
using osu.Game.Rulesets.Objects.Types;
using System.Collections.Generic;
@ -155,116 +154,76 @@ namespace osu.Game.Rulesets.Osu.Objects
{
base.CreateNestedHitObjects();
createSliderEnds();
createTicks();
createRepeatPoints();
if (LegacyLastTickOffset != null)
TailCircle.StartTime = Math.Max(StartTime + Duration / 2, TailCircle.StartTime - LegacyLastTickOffset.Value);
}
private void createSliderEnds()
{
HeadCircle = new SliderCircle
foreach (var e in
SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset))
{
StartTime = StartTime,
Position = Position,
Samples = getNodeSamples(0),
SampleControlPoint = SampleControlPoint,
IndexInCurrentCombo = IndexInCurrentCombo,
ComboIndex = ComboIndex,
};
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>();
TailCircle = new SliderTailCircle(this)
{
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
if (firstSample != null)
sampleList.Add(new SampleInfo
{
SpanIndex = span,
SpanStartTime = spanStartTime,
StartTime = spanStartTime + timeProgress * SpanDuration,
Position = Position + Path.PositionAt(distanceProgress),
StackHeight = StackHeight,
Scale = Scale,
Samples = sampleList
Bank = firstSample.Bank,
Volume = firstSample.Volume,
Name = @"slidertick",
});
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()
{
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;
}
private List<SampleInfo> getNodeSamples(int nodeIndex) =>
nodeIndex < NodeSamples.Count ? NodeSamples[nodeIndex] : Samples;
public override Judgement CreateJudgement() => new OsuJudgement();
}

View File

@ -8,6 +8,10 @@ using osu.Game.Rulesets.Osu.Judgements;
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
{
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.Game.Rulesets.Osu.Edit;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Replays.Types;
using osu.Game.Beatmaps.Legacy;
@ -128,7 +129,8 @@ namespace osu.Game.Rulesets.Osu
{
new OsuModTransform(),
new OsuModWiggle(),
new OsuModGrow()
new OsuModGrow(),
new MultiMod(new ModWindUp<OsuHitObject>(), new ModWindDown<OsuHitObject>()),
};
default:
return new Mod[] { };

View File

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

View File

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

View File

@ -17,7 +17,7 @@ using osuTK.Graphics;
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();
@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private readonly Container<Drawable> fadeContainer;
public GameplayCursor()
public GameplayCursorContainer()
{
InternalChild = fadeContainer = new Container
{
@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
public OsuCursor()
{
Origin = Anchor.Centre;
Size = new Vector2(42);
Size = new Vector2(28);
}
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.UI;
using System.Linq;
using osu.Framework.Graphics.Cursor;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.UI.Cursor;
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);
private readonly PlayfieldAdjustmentContainer adjustmentContainer;
protected override Container CursorTargetContainer => adjustmentContainer;
protected override CursorContainer CreateCursor() => new GameplayCursorContainer();
public OsuPlayfield()
{
Anchor = Anchor.Centre;
@ -29,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.UI
Size = new Vector2(0.75f);
InternalChild = new PlayfieldAdjustmentContainer
InternalChild = adjustmentContainer = new PlayfieldAdjustmentContainer
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Input;
using osu.Game.Beatmaps;
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.Replays;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
@ -59,7 +57,5 @@ namespace osu.Game.Rulesets.Osu.UI
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>
{
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!" } },
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.Taiko.Objects;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Taiko.Beatmaps;
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;
public TaikoAutoGenerator(Beatmap<TaikoHitObject> beatmap)
public TaikoAutoGenerator(IBeatmap beatmap)
: base(beatmap)
{
Replay = new Replay();

View File

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

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]
[NonParallelizable]
[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.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.ComponentModel;
using System.Linq;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
namespace osu.Game.Tests.Visual
@ -14,15 +16,27 @@ namespace osu.Game.Tests.Visual
{
protected override Player CreatePlayer(Ruleset ruleset)
{
// We create a dummy RulesetContainer just to get the replay - we don't want to use mods here
// 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);
var beatmap = Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo);
// Reset the mods
Beatmap.Value.Mods.Value = Beatmap.Value.Mods.Value.Where(m => !(m is ModAutoplay));
return new ScoreAccessibleReplayPlayer(ruleset.GetAutoplayMod().CreateReplayScore(beatmap));
}
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()
{
var toolbar = new Toolbar { State = Visibility.Visible };
ToolbarNotificationButton notificationButton = null;
Add(toolbar);
var notificationButton = toolbar.Children.OfType<FillFlowContainer>().Last().Children.OfType<ToolbarNotificationButton>().First();
AddStep("create toolbar", () =>
{
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);

View File

@ -16,7 +16,7 @@ namespace osu.Game.Tests.Visual
{
public class TestCaseUpdateableBeatmapBackgroundSprite : OsuTestCase
{
private UpdateableBeatmapBackgroundSprite backgroundSprite;
private TestUpdateableBeatmapBackgroundSprite backgroundSprite;
[Resolved]
private BeatmapManager beatmaps { get; set; }
@ -28,30 +28,36 @@ namespace osu.Game.Tests.Visual
var imported = ImportBeatmapTest.LoadOszIntoOsu(osu);
Child = backgroundSprite = new UpdateableBeatmapBackgroundSprite { RelativeSizeAxes = Axes.Both };
Child = backgroundSprite = new TestUpdateableBeatmapBackgroundSprite { RelativeSizeAxes = Axes.Both };
backgroundSprite.Beatmap.BindTo(beatmapBindable);
var req = new GetBeatmapSetRequest(1);
api.Queue(req);
AddStep("null", () => beatmapBindable.Value = null);
AddStep("imported", () => beatmapBindable.Value = imported.Beatmaps.First());
AddStep("load null beatmap", () => beatmapBindable.Value = null);
AddUntilStep(() => backgroundSprite.ChildCount == 1, "wait for cleanup...");
AddStep("load imported beatmap", () => beatmapBindable.Value = imported.Beatmaps.First());
AddUntilStep(() => backgroundSprite.ChildCount == 1, "wait for cleanup...");
if (api.IsLoggedIn)
{
AddUntilStep(() => req.Result != null, "wait for api response");
AddStep("online", () => beatmapBindable.Value = new BeatmapInfo
AddStep("load online beatmap", () => beatmapBindable.Value = new BeatmapInfo
{
BeatmapSet = req.Result?.ToBeatmapSet(rulesets)
});
AddUntilStep(() => backgroundSprite.ChildCount == 1, "wait for cleanup...");
}
else
{
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;
}
validateOnlineIds(beatmapSet.Beatmaps);
validateOnlineIds(beatmapSet);
foreach (BeatmapInfo b in 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.
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.
if (beatmapIds.GroupBy(b => b).Any(g => g.Count() > 1) || QueryBeatmaps(b => beatmapIds.Contains(b.OnlineBeatmapID)).Any())
// remove all online IDs if any problems were found.
beatmaps.ForEach(b => b.OnlineBeatmapID = null);
// ensure all IDs are unique
if (beatmapIds.GroupBy(b => b).Any(g => g.Count() > 1))
{
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>
@ -254,6 +273,18 @@ namespace osu.Game.Beatmaps
/// <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);
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>
/// Returns a list of all usable <see cref="BeatmapSetInfo"/>s.
/// </summary>

View File

@ -9,7 +9,7 @@ using osu.Framework.Graphics.Containers;
namespace osu.Game.Beatmaps.Drawables
{
/// <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>
public class UpdateableBeatmapBackgroundSprite : ModelBackedDrawable<BeatmapInfo>
{
@ -26,37 +26,45 @@ namespace osu.Game.Beatmaps.Drawables
this.beatmapSetCoverType = beatmapSetCoverType;
}
protected override Drawable CreateDrawable(BeatmapInfo model)
private BeatmapInfo lastModel;
protected override DelayedLoadWrapper CreateDelayedLoadWrapper(Drawable content, double timeBeforeLoad)
{
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 (model?.BeatmapSet?.OnlineInfo != null)
drawable = new BeatmapSetCover(model.BeatmapSet, beatmapSetCoverType);
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);
// 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;
return content;
}, timeBeforeLoad, 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}");
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)
item.Files = createFileInfos(archive, Files);
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
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>
/// Check whether an existing model already exists for a new import item.
/// </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>
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>();

View File

@ -70,13 +70,15 @@ namespace osu.Game.Online.API
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)
{
Scheduler.Add(delegate
{
components.Add(component);
component.APIStateChanged(this, state);
});
Scheduler.Add(delegate { components.Add(component); });
component.APIStateChanged(this, state);
}
public void Unregister(IOnlineComponent component)

View File

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

View File

@ -185,10 +185,7 @@ namespace osu.Game.Overlays.Direct
Margin = new MarginPadding { Top = vertical_padding, Right = vertical_padding },
Children = new[]
{
new Statistic(FontAwesome.fa_play_circle, SetInfo.OnlineInfo?.PlayCount ?? 0)
{
Margin = new MarginPadding { Right = 1 },
},
new Statistic(FontAwesome.fa_play_circle, SetInfo.OnlineInfo?.PlayCount ?? 0),
new Statistic(FontAwesome.fa_heart, SetInfo.OnlineInfo?.FavouriteCount ?? 0),
},
},

View File

@ -160,10 +160,7 @@ namespace osu.Game.Overlays.Direct
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new Statistic(FontAwesome.fa_play_circle, SetInfo.OnlineInfo?.PlayCount ?? 0)
{
Margin = new MarginPadding { Right = 1 },
},
new Statistic(FontAwesome.fa_play_circle, SetInfo.OnlineInfo?.PlayCount ?? 0),
new Statistic(FontAwesome.fa_heart, SetInfo.OnlineInfo?.FavouriteCount ?? 0),
new FillFlowContainer
{

View File

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

View File

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

View File

@ -14,10 +14,6 @@ namespace osu.Game.Rulesets.Mods
public abstract class ModAutoplay<T> : ModAutoplay, IApplicableToRulesetContainer<T>
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));
}
@ -31,5 +27,9 @@ namespace osu.Game.Rulesets.Mods
public override double ScoreMultiplier => 1;
public bool AllowFail => false;
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 string Description => "Zoooooooooom...";
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)
{

View File

@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Mods
public override ModType Type => ModType.DifficultyReduction;
public override string Description => "Less zoom...";
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)
{

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

View File

@ -42,7 +42,7 @@ namespace osu.Game.Rulesets
/// <returns>An enumerable of constructed <see cref="Mod"/>s</returns>
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)
{

View File

@ -10,6 +10,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osuTK;
@ -63,6 +64,10 @@ namespace osu.Game.Rulesets.UI
private void load(IBindable<WorkingBeatmap> beatmap)
{
this.beatmap = beatmap.Value;
Cursor = CreateCursor();
if (Cursor != null)
CursorTargetContainer.Add(Cursor);
}
/// <summary>
@ -82,6 +87,23 @@ namespace osu.Game.Rulesets.UI
/// <param name="h">The DrawableHitObject to remove.</param>
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>
/// Registers a <see cref="Playfield"/> as a nested <see cref="Playfield"/>.
/// 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.Graphics.Cursor;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Game.Configuration;
using osu.Game.Graphics.Cursor;
using osu.Game.Input.Handlers;
using osu.Game.Overlays;
using osu.Game.Replays;
@ -32,7 +34,7 @@ namespace osu.Game.Rulesets.UI
/// Should not be derived - derive <see cref="RulesetContainer{TObject}"/> instead.
/// </para>
/// </summary>
public abstract class RulesetContainer : Container
public abstract class RulesetContainer : Container, IProvideCursor
{
/// <summary>
/// The selected variant.
@ -74,10 +76,11 @@ namespace osu.Game.Rulesets.UI
/// </summary>
public Container Overlays { get; protected set; }
/// <summary>
/// The cursor provided by this <see cref="RulesetContainer"/>. May be null if no cursor is provided.
/// </summary>
public readonly CursorContainer Cursor;
public CursorContainer Cursor => Playfield.Cursor;
public bool ProvidingUserCursor => Playfield.Cursor != null && !HasReplayLoaded.Value;
protected override bool OnHover(HoverEvent e) => true; // required for IProvideCursor
public readonly Ruleset Ruleset;
@ -101,8 +104,6 @@ namespace osu.Game.Rulesets.UI
KeyBindingInputManager.UseParentInput = !paused.NewValue;
};
Cursor = CreateCursor();
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
@ -259,9 +260,6 @@ namespace osu.Game.Rulesets.UI
Playfield
});
if (Cursor != null)
KeyBindingInputManager.Add(Cursor);
InternalChildren = new Drawable[]
{
KeyBindingInputManager,

View File

@ -107,6 +107,14 @@ namespace osu.Game.Screens.Multi.Lounge
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)
{
processingOverlay.Show();

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
@ -13,6 +14,7 @@ using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics;
using osu.Game.Online.Multiplayer;
using osu.Game.Overlays.SearchableList;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Multi.Components;
using osu.Game.Screens.Play.HUD;
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();
}

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 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]
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.
pendingAnimation = true;
Progress.Value = 0;
AbortConfirm();
overlayCircle.ScaleTo(0, 100)
.Then().FadeOut().ScaleTo(1).FadeIn(500)
@ -207,6 +183,31 @@ namespace osu.Game.Screens.Play.HUD
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)
{
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 VisualSettings VisualSettings;
//public readonly CollectionSettings CollectionSettings;
//public readonly DiscussionSettings DiscussionSettings;
public PlayerSettingsOverlay()

View File

@ -1,12 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
@ -40,7 +40,9 @@ namespace osu.Game.Screens.Play
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;
@ -92,11 +94,9 @@ namespace osu.Game.Screens.Play
Progress.Objects = rulesetContainer.Objects;
Progress.AllowSeeking = rulesetContainer.HasReplayLoaded.Value;
Progress.RequestSeek = pos => adjustableClock.Seek(pos);
Progress.RequestSeek = time => RequestSeek(time);
ModDisplay.Current.BindTo(working.Mods);
PlayerSettingsOverlay.PlaybackSettings.AdjustableClock = adjustableClock;
}
[BackgroundDependencyLoader(true)]

View File

@ -7,16 +7,13 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Timing;
using osu.Game.Graphics;
using osuTK.Graphics;
namespace osu.Game.Screens.Play
{
/// <summary>
/// A container which handles pausing children, displaying a pause overlay with choices and processing the clock.
/// Exposes a <see cref="GameplayClock"/> to children via DI.
/// This alleviates a lot of the intricate pause logic from being in <see cref="Player"/>
/// A container which handles pausing children, displaying an overlay blocking its children during paused state.
/// </summary>
public class PausableGameplayContainer : Container
{
@ -44,46 +41,33 @@ namespace osu.Game.Screens.Play
public Action OnRetry;
public Action OnQuit;
private readonly FramedClock offsetClock;
private readonly DecoupleableInterpolatingFramedClock adjustableClock;
/// <summary>
/// The final clock which is exposed to underlying components.
/// </summary>
[Cached]
private readonly GameplayClock gameplayClock;
public Action Stop;
public Action Start;
/// <summary>
/// Creates a new <see cref="PausableGameplayContainer"/>.
/// </summary>
/// <param name="offsetClock">The gameplay clock. This is the clock that will process frames. Includes user/system offsets.</param>
/// <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)
public PausableGameplayContainer()
{
this.offsetClock = offsetClock;
this.adjustableClock = adjustableClock;
gameplayClock = new GameplayClock(offsetClock);
RelativeSizeAxes = Axes.Both;
AddInternal(content = new Container
InternalChildren = new[]
{
Clock = this.offsetClock,
ProcessCustomClock = false,
RelativeSizeAxes = Axes.Both
});
AddInternal(pauseOverlay = new PauseOverlay
{
OnResume = () =>
content = new Container
{
IsResuming = true;
this.Delay(400).Schedule(Resume);
RelativeSizeAxes = Axes.Both
},
OnRetry = () => OnRetry(),
OnQuit = () => OnQuit(),
});
pauseOverlay = new PauseOverlay
{
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.
@ -93,7 +77,7 @@ namespace osu.Game.Screens.Play
if (IsPaused.Value) return;
// stop the seekable clock (stops the audio eventually)
adjustableClock.Stop();
Stop?.Invoke();
IsPaused.Value = true;
pauseOverlay.Show();
@ -105,14 +89,12 @@ namespace osu.Game.Screens.Play
{
if (!IsPaused.Value) return;
IsPaused.Value = false;
IsResuming = false;
lastPauseActionTime = Time.Current;
// 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();
IsPaused.Value = false;
Start?.Invoke();
pauseOverlay.Hide();
}
@ -131,9 +113,6 @@ namespace osu.Game.Screens.Play
if (!game.IsActive.Value && CanPause)
Pause();
if (!IsPaused.Value)
offsetClock.ProcessFrame();
base.Update();
}

View File

@ -3,24 +3,19 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Input.Events;
using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Framework.Threading;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor;
using osu.Game.Online.API;
using osu.Game.Overlays;
using osu.Game.Rulesets;
@ -34,7 +29,7 @@ using osu.Game.Storyboards.Drawables;
namespace osu.Game.Screens.Play
{
public class Player : ScreenWithBeatmapBackground, IProvideCursor
public class Player : ScreenWithBeatmapBackground
{
protected override bool AllowBackButton => false; // handled by HoldForMenuButton
@ -53,22 +48,11 @@ namespace osu.Game.Screens.Play
public bool AllowResults { get; set; } = true;
private Bindable<bool> mouseWheelDisabled;
private Bindable<double> userAudioOffset;
private readonly Bindable<bool> storyboardReplacesBackground = new Bindable<bool>();
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]
private ScoreManager scoreManager { get; set; }
@ -98,25 +82,113 @@ namespace osu.Game.Screens.Play
public bool LoadedBeatmapSuccessfully => RulesetContainer?.Objects.Any() == true;
private GameplayClockContainer gameplayClockContainer;
[BackgroundDependencyLoader]
private void load(AudioManager audio, APIAccess api, OsuConfigManager config)
{
this.api = api;
WorkingBeatmap working = Beatmap.Value;
if (working is DummyWorkingBeatmap)
WorkingBeatmap working = loadBeatmap();
if (working == null)
return;
sampleRestart = audio.Sample.Get(@"Gameplay/restart");
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
{
beatmap = working.Beatmap;
var beatmap = working.Beatmap;
if (beatmap == null)
throw new InvalidOperationException("Beatmap was not loaded");
@ -140,119 +212,17 @@ namespace osu.Game.Screens.Play
if (!RulesetContainer.Objects.Any())
{
Logger.Log("Beatmap contains no hit objects!", level: LogLevel.Error);
return;
return null;
}
}
catch (Exception e)
{
Logger.Error(e, "Could not load beatmap sucessfully!");
//couldn't load, hard abort!
return;
return null;
}
sourceClock = (IAdjustableClock)working.Track ?? new StopwatchClock();
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);
return working;
}
private void performUserRequestedExit()
@ -321,7 +291,7 @@ namespace osu.Game.Screens.Play
if (Beatmap.Value.Mods.Value.OfType<IApplicableFailOverride>().Any(m => !m.AllowFail))
return false;
adjustableClock.Stop();
gameplayClockContainer.Stop();
HasFailed = true;
failOverlay.Retries = RestartCount;
@ -355,24 +325,7 @@ namespace osu.Game.Screens.Play
storyboardReplacesBackground.Value = Beatmap.Value.Storyboard.ReplacesBackground && Beatmap.Value.Storyboard.HasDrawable;
Task.Run(() =>
{
sourceClock.Reset();
Schedule(() =>
{
adjustableClock.ChangeSource(sourceClock);
applyRateFromMods();
this.Delay(750).Schedule(() =>
{
if (!PausableGameplayContainer.IsPaused.Value)
{
adjustableClock.Start();
}
});
});
});
gameplayClockContainer.Restart();
PausableGameplayContainer.Alpha = 0;
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))
{
// In the case of replays, we may have changed the playback rate.
applyRateFromMods();
gameplayClockContainer.ResetLocalAdjustments();
fadeOut();
return base.OnExiting(next);
}

View File

@ -4,7 +4,6 @@
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Timing;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
@ -16,7 +15,13 @@ namespace osu.Game.Screens.Play.PlayerSettings
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;
@ -47,31 +52,13 @@ namespace osu.Game.Screens.Play.PlayerSettings
}
},
},
rateSlider = new PlayerSliderBar<double>
{
Bindable = new BindableDouble(1)
{
Default = 1,
MinValue = 0.5,
MaxValue = 2,
Precision = 0.1,
},
}
rateSlider = new PlayerSliderBar<double> { Bindable = UserPlaybackRate }
};
}
protected override void 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);
}
}

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Humanizer;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@ -64,7 +65,7 @@ namespace osu.Game.Screens.Select
{
Ruleset.Value = CurrentItem.Value.Ruleset;
Beatmap.Value = beatmaps.GetWorkingBeatmap(CurrentItem.Value.Beatmap);
Beatmap.Value.Mods.Value = selectedMods.Value = CurrentItem.Value.RequiredMods;
Beatmap.Value.Mods.Value = selectedMods.Value = CurrentItem.Value.RequiredMods ?? Enumerable.Empty<Mod>();
}
Beatmap.Disabled = true;

View File

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

View File

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

View File

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