Merge branch 'master' into channel-selector-tab-item

This commit is contained in:
Dean Herbert
2017-05-22 20:22:57 +09:00
committed by GitHub
95 changed files with 2632 additions and 991 deletions

View File

@ -85,25 +85,25 @@ namespace osu.Desktop.VisualTests.Tests
Clock = new FramedClock(), Clock = new FramedClock(),
Children = new Drawable[] Children = new Drawable[]
{ {
new OsuHitRenderer(beatmap) new OsuHitRenderer(beatmap, false)
{ {
Scale = new Vector2(0.5f), Scale = new Vector2(0.5f),
Anchor = Anchor.TopLeft, Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft Origin = Anchor.TopLeft
}, },
new TaikoHitRenderer(beatmap) new TaikoHitRenderer(beatmap, false)
{ {
Scale = new Vector2(0.5f), Scale = new Vector2(0.5f),
Anchor = Anchor.TopRight, Anchor = Anchor.TopRight,
Origin = Anchor.TopRight Origin = Anchor.TopRight
}, },
new CatchHitRenderer(beatmap) new CatchHitRenderer(beatmap, false)
{ {
Scale = new Vector2(0.5f), Scale = new Vector2(0.5f),
Anchor = Anchor.BottomLeft, Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft Origin = Anchor.BottomLeft
}, },
new ManiaHitRenderer(beatmap) new ManiaHitRenderer(beatmap, false)
{ {
Scale = new Vector2(0.5f), Scale = new Vector2(0.5f),
Anchor = Anchor.BottomRight, Anchor = Anchor.BottomRight,

View File

@ -129,8 +129,6 @@ namespace osu.Desktop.VisualTests.Tests
}; };
Add(clockAdjustContainer); Add(clockAdjustContainer);
load(mode);
} }
private int depth; private int depth;

View File

@ -43,13 +43,8 @@ namespace osu.Desktop.VisualTests.Tests
RelativeCoordinateSpace = new Vector2(1, 10000), RelativeCoordinateSpace = new Vector2(1, 10000),
Children = new[] Children = new[]
{ {
new DrawableNote(new Note new DrawableNote(new Note { StartTime = 5000 }) { AccentColour = Color4.Red },
{ new DrawableNote(new Note { StartTime = 6000 }) { AccentColour = Color4.Red }
StartTime = 5000
})
{
AccentColour = Color4.Red
}
} }
} }
} }
@ -74,10 +69,7 @@ namespace osu.Desktop.VisualTests.Tests
{ {
StartTime = 5000, StartTime = 5000,
Duration = 1000 Duration = 1000
}) }) { AccentColour = Color4.Red }
{
AccentColour = Color4.Red
}
} }
} }
} }

View File

@ -12,7 +12,7 @@ namespace osu.Desktop.VisualTests.Tests
{ {
public override string Description => @"Tests pause and fail overlays"; public override string Description => @"Tests pause and fail overlays";
private PauseOverlay pauseOverlay; private PauseContainer.PauseOverlay pauseOverlay;
private FailOverlay failOverlay; private FailOverlay failOverlay;
private int retryCount; private int retryCount;
@ -22,7 +22,7 @@ namespace osu.Desktop.VisualTests.Tests
retryCount = 0; retryCount = 0;
Add(pauseOverlay = new PauseOverlay Add(pauseOverlay = new PauseContainer.PauseOverlay
{ {
OnResume = () => Logger.Log(@"Resume"), OnResume = () => Logger.Log(@"Resume"),
OnRetry = () => Logger.Log(@"Retry"), OnRetry = () => Logger.Log(@"Retry"),

View File

@ -16,7 +16,7 @@ namespace osu.Desktop.VisualTests.Tests
base.Reset(); base.Reset();
Add(new BackButton()); Add(new BackButton());
Add(new SkipButton()); Add(new SkipButton(Clock.CurrentTime + 5000));
} }
} }
} }

View File

@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Catch
{ {
public class CatchRuleset : Ruleset public class CatchRuleset : Ruleset
{ {
public override HitRenderer CreateHitRendererWith(WorkingBeatmap beatmap) => new CatchHitRenderer(beatmap); public override HitRenderer CreateHitRendererWith(WorkingBeatmap beatmap, bool isForCurrentRuleset) => new CatchHitRenderer(beatmap, isForCurrentRuleset);
public override IEnumerable<Mod> GetModsFor(ModType type) public override IEnumerable<Mod> GetModsFor(ModType type)
{ {

View File

@ -15,8 +15,8 @@ namespace osu.Game.Rulesets.Catch.UI
{ {
public class CatchHitRenderer : HitRenderer<CatchBaseHit, CatchJudgement> public class CatchHitRenderer : HitRenderer<CatchBaseHit, CatchJudgement>
{ {
public CatchHitRenderer(WorkingBeatmap beatmap) public CatchHitRenderer(WorkingBeatmap beatmap, bool isForCurrentRuleset)
: base(beatmap) : base(beatmap, isForCurrentRuleset)
{ {
} }

View File

@ -1,35 +1,150 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>. // Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using System.Collections.Generic;
using System;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Beatmaps; using osu.Game.Rulesets.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using System;
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using OpenTK; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Mania.Beatmaps.Patterns;
using osu.Game.Rulesets.Mania.MathUtils;
using osu.Game.Database;
using osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy;
namespace osu.Game.Rulesets.Mania.Beatmaps namespace osu.Game.Rulesets.Mania.Beatmaps
{ {
internal class ManiaBeatmapConverter : BeatmapConverter<ManiaHitObject> public class ManiaBeatmapConverter : BeatmapConverter<ManiaHitObject>
{ {
protected override IEnumerable<Type> ValidConversionTypes { get; } = new[] { typeof(IHasXPosition) }; protected override IEnumerable<Type> ValidConversionTypes { get; } = new[] { typeof(IHasXPosition) };
private Pattern lastPattern = new Pattern();
private FastRandom random;
private Beatmap beatmap;
private bool isForCurrentRuleset;
protected override Beatmap<ManiaHitObject> ConvertBeatmap(Beatmap original, bool isForCurrentRuleset)
{
this.isForCurrentRuleset = isForCurrentRuleset;
beatmap = original;
BeatmapDifficulty difficulty = original.BeatmapInfo.Difficulty;
int seed = (int)Math.Round(difficulty.DrainRate + difficulty.CircleSize) * 20 + (int)(difficulty.OverallDifficulty * 41.2) + (int)Math.Round(difficulty.ApproachRate);
random = new FastRandom(seed);
return base.ConvertBeatmap(original, isForCurrentRuleset);
}
protected override IEnumerable<ManiaHitObject> ConvertHitObject(HitObject original, Beatmap beatmap) protected override IEnumerable<ManiaHitObject> ConvertHitObject(HitObject original, Beatmap beatmap)
{ {
int availableColumns = (int)Math.Round(beatmap.BeatmapInfo.Difficulty.CircleSize); var maniaOriginal = original as ManiaHitObject;
if (maniaOriginal != null)
var positionData = original as IHasXPosition;
float localWDivisor = 512.0f / availableColumns;
int column = MathHelper.Clamp((int)Math.Floor((positionData?.X ?? 1) / localWDivisor), 0, availableColumns - 1);
yield return new Note
{ {
StartTime = original.StartTime, yield return maniaOriginal;
Column = column, yield break;
}; }
var objects = isForCurrentRuleset ? generateSpecific(original) : generateConverted(original);
if (objects == null)
yield break;
foreach (ManiaHitObject obj in objects)
yield return obj;
}
/// <summary>
/// Method that generates hit objects for osu!mania specific beatmaps.
/// </summary>
/// <param name="original">The original hit object.</param>
/// <returns>The hit objects generated.</returns>
private IEnumerable<ManiaHitObject> generateSpecific(HitObject original)
{
var generator = new SpecificBeatmapPatternGenerator(random, original, beatmap, lastPattern);
Pattern newPattern = generator.Generate();
lastPattern = newPattern;
return newPattern.HitObjects;
}
/// <summary>
/// Method that generates hit objects for non-osu!mania beatmaps.
/// </summary>
/// <param name="original">The original hit object.</param>
/// <returns>The hit objects generated.</returns>
private IEnumerable<ManiaHitObject> generateConverted(HitObject original)
{
var endTimeData = original as IHasEndTime;
var distanceData = original as IHasDistance;
var positionData = original as IHasPosition;
// Following lines currently commented out to appease resharper
Patterns.PatternGenerator conversion = null;
if (distanceData != null)
conversion = new DistanceObjectPatternGenerator(random, original, beatmap, lastPattern);
else if (endTimeData != null)
conversion = new EndTimeObjectPatternGenerator(random, original, beatmap);
else if (positionData != null)
{
// Circle
}
if (conversion == null)
return null;
Pattern newPattern = conversion.Generate();
lastPattern = newPattern;
return newPattern.HitObjects;
}
/// <summary>
/// A pattern generator for osu!mania-specific beatmaps.
/// </summary>
private class SpecificBeatmapPatternGenerator : Patterns.Legacy.PatternGenerator
{
public SpecificBeatmapPatternGenerator(FastRandom random, HitObject hitObject, Beatmap beatmap, Pattern previousPattern)
: base(random, hitObject, beatmap, previousPattern)
{
}
public override Pattern Generate()
{
var endTimeData = HitObject as IHasEndTime;
var positionData = HitObject as IHasXPosition;
int column = GetColumn(positionData?.X ?? 0);
var pattern = new Pattern();
if (endTimeData != null)
{
pattern.Add(new HoldNote
{
StartTime = HitObject.StartTime,
Samples = HitObject.Samples,
Duration = endTimeData.Duration,
Column = column,
});
}
else if (positionData != null)
{
pattern.Add(new Note
{
StartTime = HitObject.StartTime,
Samples = HitObject.Samples,
Column = column
});
}
return pattern;
}
} }
} }
} }

View File

@ -0,0 +1,490 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Linq;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Mania.MathUtils;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Mania.Objects;
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{
/// <summary>
/// A pattern generator for IHasDistance hit objects.
/// </summary>
internal class DistanceObjectPatternGenerator : PatternGenerator
{
/// <summary>
/// Base osu! slider scoring distance.
/// </summary>
private const float osu_base_scoring_distance = 100;
private readonly double endTime;
private readonly double segmentDuration;
private readonly int repeatCount;
private PatternType convertType;
public DistanceObjectPatternGenerator(FastRandom random, HitObject hitObject, Beatmap beatmap, Pattern previousPattern)
: base(random, hitObject, beatmap, previousPattern)
{
ControlPoint overridePoint;
ControlPoint controlPoint = Beatmap.TimingInfo.TimingPointAt(hitObject.StartTime, out overridePoint);
convertType = PatternType.None;
if ((overridePoint ?? controlPoint)?.KiaiMode == false)
convertType = PatternType.LowProbability;
var distanceData = hitObject as IHasDistance;
var repeatsData = hitObject as IHasRepeats;
repeatCount = repeatsData?.RepeatCount ?? 1;
double speedAdjustment = beatmap.TimingInfo.SpeedMultiplierAt(hitObject.StartTime);
double speedAdjustedBeatLength = beatmap.TimingInfo.BeatLengthAt(hitObject.StartTime) * speedAdjustment;
// The true distance, accounting for any repeats
double distance = (distanceData?.Distance ?? 0) * repeatCount;
// The velocity of the osu! hit object - calculated as the velocity of a slider
double osuVelocity = osu_base_scoring_distance * beatmap.BeatmapInfo.Difficulty.SliderMultiplier / speedAdjustedBeatLength;
// The duration of the osu! hit object
double osuDuration = distance / osuVelocity;
endTime = hitObject.StartTime + osuDuration;
segmentDuration = (endTime - HitObject.StartTime) / repeatCount;
}
public override Pattern Generate()
{
if (repeatCount > 1)
{
if (segmentDuration <= 90)
return generateRandomHoldNotes(HitObject.StartTime, 1);
if (segmentDuration <= 120)
{
convertType |= PatternType.ForceNotStack;
return generateRandomNotes(HitObject.StartTime, repeatCount + 1);
}
if (segmentDuration <= 160)
return generateStair(HitObject.StartTime);
if (segmentDuration <= 200 && ConversionDifficulty > 3)
return generateRandomMultipleNotes(HitObject.StartTime);
double duration = endTime - HitObject.StartTime;
if (duration >= 4000)
return generateNRandomNotes(HitObject.StartTime, 0.23, 0, 0);
if (segmentDuration > 400 && repeatCount < AvailableColumns - 1 - RandomStart)
return generateTiledHoldNotes(HitObject.StartTime);
return generateHoldAndNormalNotes(HitObject.StartTime);
}
if (segmentDuration <= 110)
{
if (PreviousPattern.ColumnWithObjects < AvailableColumns)
convertType |= PatternType.ForceNotStack;
else
convertType &= ~PatternType.ForceNotStack;
return generateRandomNotes(HitObject.StartTime, segmentDuration < 80 ? 1 : 2);
}
if (ConversionDifficulty > 6.5)
{
if ((convertType & PatternType.LowProbability) > 0)
return generateNRandomNotes(HitObject.StartTime, 0.78, 0.3, 0);
return generateNRandomNotes(HitObject.StartTime, 0.85, 0.36, 0.03);
}
if (ConversionDifficulty > 4)
{
if ((convertType & PatternType.LowProbability) > 0)
return generateNRandomNotes(HitObject.StartTime, 0.43, 0.08, 0);
return generateNRandomNotes(HitObject.StartTime, 0.56, 0.18, 0);
}
if (ConversionDifficulty > 2.5)
{
if ((convertType & PatternType.LowProbability) > 0)
return generateNRandomNotes(HitObject.StartTime, 0.3, 0, 0);
return generateNRandomNotes(HitObject.StartTime, 0.37, 0.08, 0);
}
if ((convertType & PatternType.LowProbability) > 0)
return generateNRandomNotes(HitObject.StartTime, 0.17, 0, 0);
return generateNRandomNotes(HitObject.StartTime, 0.27, 0, 0);
}
/// <summary>
/// Generates random hold notes that start at an span the same amount of rows.
/// </summary>
/// <param name="startTime">Start time of each hold note.</param>
/// <param name="noteCount">Number of hold notes.</param>
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
private Pattern generateRandomHoldNotes(double startTime, int noteCount)
{
// - - - -
// ■ - ■ ■
// □ - □ □
// ■ - ■ ■
var pattern = new Pattern();
int usableColumns = AvailableColumns - RandomStart - PreviousPattern.ColumnWithObjects;
int nextColumn = Random.Next(RandomStart, AvailableColumns);
for (int i = 0; i < Math.Min(usableColumns, noteCount); i++)
{
while (pattern.ColumnHasObject(nextColumn) || PreviousPattern.ColumnHasObject(nextColumn)) //find available column
nextColumn = Random.Next(RandomStart, AvailableColumns);
addToPattern(pattern, nextColumn, startTime, endTime);
}
// This is can't be combined with the above loop due to RNG
for (int i = 0; i < noteCount - usableColumns; i++)
{
while (pattern.ColumnHasObject(nextColumn))
nextColumn = Random.Next(RandomStart, AvailableColumns);
addToPattern(pattern, nextColumn, startTime, endTime);
}
return pattern;
}
/// <summary>
/// Generates random notes, with one note per row and no stacking.
/// </summary>
/// <param name="startTime">The start time.</param>
/// <param name="noteCount">The number of notes.</param>
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
private Pattern generateRandomNotes(double startTime, int noteCount)
{
// - - - -
// x - - -
// - - x -
// - - - x
// x - - -
var pattern = new Pattern();
int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
if ((convertType & PatternType.ForceNotStack) > 0 && PreviousPattern.ColumnWithObjects < AvailableColumns)
{
while (PreviousPattern.ColumnHasObject(nextColumn))
nextColumn = Random.Next(RandomStart, AvailableColumns);
}
int lastColumn = nextColumn;
for (int i = 0; i < noteCount; i++)
{
addToPattern(pattern, nextColumn, startTime, startTime);
while (nextColumn == lastColumn)
nextColumn = Random.Next(RandomStart, AvailableColumns);
lastColumn = nextColumn;
startTime += segmentDuration;
}
return pattern;
}
/// <summary>
/// Generates a stair of notes, with one note per row.
/// </summary>
/// <param name="startTime">The start time.</param>
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
private Pattern generateStair(double startTime)
{
// - - - -
// x - - -
// - x - -
// - - x -
// - - - x
// - - x -
// - x - -
// x - - -
var pattern = new Pattern();
int column = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
bool increasing = Random.NextDouble() > 0.5;
for (int i = 0; i <= repeatCount; i++)
{
addToPattern(pattern, column, startTime, startTime);
startTime += segmentDuration;
// Check if we're at the borders of the stage, and invert the pattern if so
if (increasing)
{
if (column >= AvailableColumns - 1)
{
increasing = false;
column--;
}
else
column++;
}
else
{
if (column <= RandomStart)
{
increasing = true;
column++;
}
else
column--;
}
}
return pattern;
}
/// <summary>
/// Generates random notes with 1-2 notes per row and no stacking.
/// </summary>
/// <param name="startTime">The start time.</param>
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
private Pattern generateRandomMultipleNotes(double startTime)
{
// - - - -
// x - - -
// - x x -
// - - - x
// x - x -
var pattern = new Pattern();
bool legacy = AvailableColumns >= 4 && AvailableColumns <= 8;
int interval = Random.Next(1, AvailableColumns - (legacy ? 1 : 0));
int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
for (int i = 0; i <= repeatCount; i++)
{
addToPattern(pattern, nextColumn, startTime, startTime);
nextColumn += interval;
if (nextColumn >= AvailableColumns - RandomStart)
nextColumn = nextColumn - AvailableColumns - RandomStart + (legacy ? 1 : 0);
nextColumn += RandomStart;
// If we're in 2K, let's not add many consecutive doubles
if (AvailableColumns > 2)
addToPattern(pattern, nextColumn, startTime, startTime);
nextColumn = Random.Next(RandomStart, AvailableColumns);
startTime += segmentDuration;
}
return pattern;
}
/// <summary>
/// Generates random hold notes. The amount of hold notes generated is determined by probabilities.
/// </summary>
/// <param name="startTime">The hold note start time.</param>
/// <param name="p2">The probability required for 2 hold notes to be generated.</param>
/// <param name="p3">The probability required for 3 hold notes to be generated.</param>
/// <param name="p4">The probability required for 4 hold notes to be generated.</param>
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
private Pattern generateNRandomNotes(double startTime, double p2, double p3, double p4)
{
// - - - -
// ■ - ■ ■
// □ - □ □
// ■ - ■ ■
switch (AvailableColumns)
{
case 2:
p2 = 0;
p3 = 0;
p4 = 0;
break;
case 3:
p2 = Math.Max(p2, 0.1);
p3 = 0;
p4 = 0;
break;
case 4:
p2 = Math.Max(p2, 0.3);
p3 = Math.Max(p3, 0.04);
p4 = 0;
break;
case 5:
p2 = Math.Max(p2, 0.34);
p3 = Math.Max(p3, 0.1);
p4 = Math.Max(p4, 0.03);
break;
}
Func<SampleInfo, bool> isDoubleSample = sample => sample.Name == SampleInfo.HIT_CLAP && sample.Name == SampleInfo.HIT_FINISH;
bool canGenerateTwoNotes = (convertType & PatternType.LowProbability) == 0;
canGenerateTwoNotes &= HitObject.Samples.Any(isDoubleSample) || sampleInfoListAt(HitObject.StartTime).Any(isDoubleSample);
if (canGenerateTwoNotes)
p2 = 1;
return generateRandomHoldNotes(startTime, GetRandomNoteCount(p2, p3, p4));
}
/// <summary>
/// Generates tiled hold notes. You can think of this as a stair of hold notes.
/// </summary>
/// <param name="startTime">The first hold note start time.</param>
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
private Pattern generateTiledHoldNotes(double startTime)
{
// - - - -
// ■ ■ ■ ■
// □ □ □ □
// □ □ □ □
// □ □ □ ■
// □ □ ■ -
// □ ■ - -
// ■ - - -
var pattern = new Pattern();
int columnRepeat = Math.Min(repeatCount, AvailableColumns);
int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
if ((convertType & PatternType.ForceNotStack) > 0 && PreviousPattern.ColumnWithObjects < AvailableColumns)
{
while (PreviousPattern.ColumnHasObject(nextColumn))
nextColumn = Random.Next(RandomStart, AvailableColumns);
}
for (int i = 0; i < columnRepeat; i++)
{
while (pattern.ColumnHasObject(nextColumn))
nextColumn = Random.Next(RandomStart, AvailableColumns);
addToPattern(pattern, nextColumn, startTime, endTime);
startTime += segmentDuration;
}
return pattern;
}
/// <summary>
/// Generates a hold note alongside normal notes.
/// </summary>
/// <param name="startTime">The start time of notes.</param>
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
private Pattern generateHoldAndNormalNotes(double startTime)
{
// - - - -
// ■ x x -
// ■ - x x
// ■ x - x
// ■ - x x
var pattern = new Pattern();
int holdColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
if ((convertType & PatternType.ForceNotStack) > 0 && PreviousPattern.ColumnWithObjects < AvailableColumns)
{
while (PreviousPattern.ColumnHasObject(holdColumn))
holdColumn = Random.Next(RandomStart, AvailableColumns);
}
// Create the hold note
addToPattern(pattern, holdColumn, startTime, endTime);
int noteCount = 1;
if (ConversionDifficulty > 6.5)
noteCount = GetRandomNoteCount(0.63, 0);
else if (ConversionDifficulty > 4)
noteCount = GetRandomNoteCount(AvailableColumns < 6 ? 0.12 : 0.45, 0);
else if (ConversionDifficulty > 2.5)
noteCount = GetRandomNoteCount(AvailableColumns < 6 ? 0 : 0.24, 0);
noteCount = Math.Min(AvailableColumns - 1, noteCount);
bool ignoreHead = !sampleInfoListAt(startTime).Any(s => s.Name == SampleInfo.HIT_WHISTLE || s.Name == SampleInfo.HIT_FINISH || s.Name == SampleInfo.HIT_CLAP);
int nextColumn = Random.Next(RandomStart, AvailableColumns);
var rowPattern = new Pattern();
for (int i = 0; i <= repeatCount; i++)
{
if (!(ignoreHead && startTime == HitObject.StartTime))
{
for (int j = 0; j < noteCount; j++)
{
while (rowPattern.ColumnHasObject(nextColumn) || nextColumn == holdColumn)
nextColumn = Random.Next(RandomStart, AvailableColumns);
addToPattern(rowPattern, nextColumn, startTime, startTime);
}
}
pattern.Add(rowPattern);
rowPattern.Clear();
startTime += segmentDuration;
}
return pattern;
}
/// <summary>
/// Retrieves the sample info list at a point in time.
/// </summary>
/// <param name="time">The time to retrieve the sample info list from.</param>
/// <returns></returns>
private SampleInfoList sampleInfoListAt(double time)
{
var curveData = HitObject as IHasCurve;
if (curveData == null)
return HitObject.Samples;
double segmentTime = (endTime - HitObject.StartTime) / repeatCount;
int index = (int)(segmentTime == 0 ? 0 : (time - HitObject.StartTime) / segmentTime);
return curveData.RepeatSamples[index];
}
/// <summary>
/// Constructs and adds a note to a pattern.
/// </summary>
/// <param name="pattern">The pattern to add to.</param>
/// <param name="column">The column to add the note to.</param>
/// <param name="startTime">The start time of the note.</param>
/// <param name="endTime">The end time of the note (set to <paramref name="startTime"/> for a non-hold note).</param>
private void addToPattern(Pattern pattern, int column, double startTime, double endTime)
{
ManiaHitObject newObject;
if (startTime == endTime)
{
newObject = new Note
{
StartTime = startTime,
Samples = sampleInfoListAt(startTime),
Column = column
};
}
else
{
newObject = new HoldNote
{
StartTime = startTime,
Samples = sampleInfoListAt(startTime),
EndSamples = sampleInfoListAt(endTime),
Column = column,
Duration = endTime - startTime
};
}
pattern.Add(newObject);
}
}
}

View File

@ -0,0 +1,98 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.MathUtils;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using System.Linq;
using osu.Game.Audio;
using osu.Game.Rulesets.Mania.Objects;
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{
internal class EndTimeObjectPatternGenerator : PatternGenerator
{
private readonly double endTime;
public EndTimeObjectPatternGenerator(FastRandom random, HitObject hitObject, Beatmap beatmap)
: base(random, hitObject, beatmap, new Pattern())
{
var endtimeData = HitObject as IHasEndTime;
endTime = endtimeData?.EndTime ?? 0;
}
public override Pattern Generate()
{
var pattern = new Pattern();
bool generateHold = endTime - HitObject.StartTime >= 100;
if (AvailableColumns == 8)
{
if (HitObject.Samples.Any(s => s.Name == SampleInfo.HIT_FINISH) && endTime - HitObject.StartTime < 1000)
addToPattern(pattern, 0, generateHold);
else
addToPattern(pattern, getNextRandomColumn(RandomStart), generateHold);
}
else if (AvailableColumns > 0)
addToPattern(pattern, getNextRandomColumn(0), generateHold);
return pattern;
}
/// <summary>
/// Picks a random column after a column.
/// </summary>
/// <param name="start">The starting column.</param>
/// <returns>A random column after <paramref name="start"/>.</returns>
private int getNextRandomColumn(int start)
{
int nextColumn = Random.Next(start, AvailableColumns);
while (PreviousPattern.ColumnHasObject(nextColumn))
nextColumn = Random.Next(start, AvailableColumns);
return nextColumn;
}
/// <summary>
/// Constructs and adds a note to a pattern.
/// </summary>
/// <param name="pattern">The pattern to add to.</param>
/// <param name="column">The column to add the note to.</param>
/// <param name="holdNote">Whether to add a hold note.</param>
private void addToPattern(Pattern pattern, int column, bool holdNote)
{
ManiaHitObject newObject;
if (holdNote)
{
newObject = new HoldNote
{
StartTime = HitObject.StartTime,
EndSamples = HitObject.Samples,
Column = column,
Duration = endTime - HitObject.StartTime
};
newObject.Samples.Add(new SampleInfo
{
Name = SampleInfo.HIT_NORMAL
});
}
else
{
newObject = new Note
{
StartTime = HitObject.StartTime,
Samples = HitObject.Samples,
Column = column
};
}
pattern.Add(newObject);
}
}
}

View File

@ -0,0 +1,106 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Rulesets.Mania.MathUtils;
using osu.Game.Rulesets.Objects;
using OpenTK;
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{
/// <summary>
/// A pattern generator for legacy hit objects.
/// </summary>
internal abstract class PatternGenerator : Patterns.PatternGenerator
{
/// <summary>
/// The column index at which to start generating random notes.
/// </summary>
protected readonly int RandomStart;
/// <summary>
/// The random number generator to use.
/// </summary>
protected readonly FastRandom Random;
protected PatternGenerator(FastRandom random, HitObject hitObject, Beatmap beatmap, Pattern previousPattern)
: base(hitObject, beatmap, previousPattern)
{
Random = random;
RandomStart = AvailableColumns == 8 ? 1 : 0;
}
/// <summary>
/// Converts an x-position into a column.
/// </summary>
/// <param name="position">The x-position.</param>
/// <param name="allowSpecial">Whether to treat as 7K + 1.</param>
/// <returns>The column.</returns>
protected int GetColumn(float position, bool allowSpecial = false)
{
if (allowSpecial && AvailableColumns == 8)
{
const float local_x_divisor = 512f / 7;
return MathHelper.Clamp((int)Math.Floor(position / local_x_divisor), 0, 6) + 1;
}
float localXDivisor = 512f / AvailableColumns;
return MathHelper.Clamp((int)Math.Floor(position / localXDivisor), 0, AvailableColumns - 1);
}
/// <summary>
/// Generates a count of notes to be generated from probabilities.
/// </summary>
/// <param name="p2">Probability for 2 notes to be generated.</param>
/// <param name="p3">Probability for 3 notes to be generated.</param>
/// <param name="p4">Probability for 4 notes to be generated.</param>
/// <param name="p5">Probability for 5 notes to be generated.</param>
/// <param name="p6">Probability for 6 notes to be generated.</param>
/// <returns>The amount of notes to be generated.</returns>
protected int GetRandomNoteCount(double p2, double p3, double p4 = 0, double p5 = 0, double p6 = 0)
{
double val = Random.NextDouble();
if (val >= 1 - p6)
return 6;
if (val >= 1 - p5)
return 5;
if (val >= 1 - p4)
return 4;
if (val >= 1 - p3)
return 3;
return val >= 1 - p2 ? 2 : 1;
}
private double? conversionDifficulty;
/// <summary>
/// A difficulty factor used for various conversion methods from osu!stable.
/// </summary>
protected double ConversionDifficulty
{
get
{
if (conversionDifficulty != null)
return conversionDifficulty.Value;
HitObject lastObject = Beatmap.HitObjects.LastOrDefault();
HitObject firstObject = Beatmap.HitObjects.FirstOrDefault();
double drainTime = (lastObject?.StartTime ?? 0) - (firstObject?.StartTime ?? 0);
drainTime -= Beatmap.TotalBreakTime;
if (drainTime == 0)
drainTime = 10000;
BeatmapDifficulty difficulty = Beatmap.BeatmapInfo.Difficulty;
conversionDifficulty = ((difficulty.DrainRate + MathHelper.Clamp(difficulty.ApproachRate, 4, 7)) / 1.5 + Beatmap.HitObjects.Count / drainTime * 9f) / 38f * 5f / 1.15;
conversionDifficulty = Math.Min(conversionDifficulty.Value, 12);
return conversionDifficulty.Value;
}
}
}
}

View File

@ -0,0 +1,65 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{
/// <summary>
/// The type of pattern to generate. Used for legacy patterns.
/// </summary>
[Flags]
internal enum PatternType
{
None = 0,
/// <summary>
/// Keep the same as last row.
/// </summary>
ForceStack = 1 << 0,
/// <summary>
/// Keep different from last row.
/// </summary>
ForceNotStack = 1 << 1,
/// <summary>
/// Keep as single note at its original position.
/// </summary>
KeepSingle = 1 << 2,
/// <summary>
/// Use a lower random value.
/// </summary>
LowProbability = 1 << 3,
/// <summary>
/// Reserved.
/// </summary>
Alternate = 1 << 4,
/// <summary>
/// Ignore the repeat count.
/// </summary>
ForceSigSlider = 1 << 5,
/// <summary>
/// Convert slider to circle.
/// </summary>
ForceNotSlider = 1 << 6,
/// <summary>
/// Notes gathered together.
/// </summary>
Gathered = 1 << 7,
Mirror = 1 << 8,
/// <summary>
/// Change 0 -> 6.
/// </summary>
Reverse = 1 << 9,
/// <summary>
/// 1 -> 5 -> 1 -> 5 like reverse.
/// </summary>
Cycle = 1 << 10,
/// <summary>
/// Next note will be at column + 1.
/// </summary>
Stair = 1 << 11,
/// <summary>
/// Next note will be at column - 1.
/// </summary>
ReverseStair = 1 << 12
}
}

View File

@ -0,0 +1,57 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Mania.Objects;
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns
{
/// <summary>
/// Creates a pattern containing hit objects.
/// </summary>
internal class Pattern
{
private readonly List<ManiaHitObject> hitObjects = new List<ManiaHitObject>();
/// <summary>
/// All the hit objects contained in this pattern.
/// </summary>
public IEnumerable<ManiaHitObject> HitObjects => hitObjects;
/// <summary>
/// Check whether a column of this patterns contains a hit object.
/// </summary>
/// <param name="column">The column index.</param>
/// <returns>Whether the column with index <paramref name="column"/> contains a hit object.</returns>
public bool ColumnHasObject(int column) => hitObjects.Exists(h => h.Column == column);
/// <summary>
/// Amount of columns taken up by hit objects in this pattern.
/// </summary>
public int ColumnWithObjects => HitObjects.GroupBy(h => h.Column).Count();
/// <summary>
/// Adds a hit object to this pattern.
/// </summary>
/// <param name="hitObject">The hit object to add.</param>
public void Add(ManiaHitObject hitObject) => hitObjects.Add(hitObject);
/// <summary>
/// Copies hit object from another pattern to this one.
/// </summary>
/// <param name="other">The other pattern.</param>
public void Add(Pattern other) => hitObjects.AddRange(other.HitObjects);
/// <summary>
/// Clears this pattern, removing all hit objects.
/// </summary>
public void Clear() => hitObjects.Clear();
/// <summary>
/// Removes a hit object from this pattern.
/// </summary>
/// <param name="hitObject">The hit object to remove.</param>
public bool Remove(ManiaHitObject hitObject) => hitObjects.Remove(hitObject);
}
}

View File

@ -0,0 +1,50 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns
{
/// <summary>
/// Generator to create a pattern <see cref="Pattern"/> from a hit object.
/// </summary>
internal abstract class PatternGenerator
{
/// <summary>
/// The number of columns available to create the pattern.
/// </summary>
protected readonly int AvailableColumns;
/// <summary>
/// The last pattern.
/// </summary>
protected readonly Pattern PreviousPattern;
/// <summary>
/// The hit object to create the pattern for.
/// </summary>
protected readonly HitObject HitObject;
/// <summary>
/// The beatmap which <see cref="HitObject"/> is a part of.
/// </summary>
protected readonly Beatmap Beatmap;
protected PatternGenerator(HitObject hitObject, Beatmap beatmap, Pattern previousPattern)
{
PreviousPattern = previousPattern;
HitObject = hitObject;
Beatmap = beatmap;
AvailableColumns = (int)Math.Round(beatmap.BeatmapInfo.Difficulty.CircleSize);
}
/// <summary>
/// Generates the pattern for <see cref="HitObject"/>, filled with hit objects.
/// </summary>
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
public abstract Pattern Generate();
}
}

View File

@ -140,6 +140,26 @@ namespace osu.Game.Rulesets.Mania.Judgements
Miss = BeatmapDifficulty.DifficultyRange(difficulty, miss_max, miss_mid, miss_min); Miss = BeatmapDifficulty.DifficultyRange(difficulty, miss_max, miss_mid, miss_min);
} }
/// <summary>
/// Retrieves the hit result for a time offset.
/// </summary>
/// <param name="hitOffset">The time offset.</param>
/// <returns>The hit result, or null if the time offset results in a miss.</returns>
public ManiaHitResult? ResultFor(double hitOffset)
{
if (hitOffset <= Perfect / 2)
return ManiaHitResult.Perfect;
if (hitOffset <= Great / 2)
return ManiaHitResult.Great;
if (hitOffset <= Good / 2)
return ManiaHitResult.Good;
if (hitOffset <= Ok / 2)
return ManiaHitResult.Ok;
if (hitOffset <= Bad / 2)
return ManiaHitResult.Bad;
return null;
}
/// <summary> /// <summary>
/// Constructs new hit windows which have been multiplied by a value. /// Constructs new hit windows which have been multiplied by a value.
/// </summary> /// </summary>

View File

@ -0,0 +1,21 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.ComponentModel;
namespace osu.Game.Rulesets.Mania.Judgements
{
public enum ManiaHitResult
{
[Description("PERFECT")]
Perfect,
[Description("GREAT")]
Great,
[Description("GOOD")]
Good,
[Description("OK")]
Ok,
[Description("BAD")]
Bad
}
}

View File

@ -10,5 +10,10 @@ namespace osu.Game.Rulesets.Mania.Judgements
public override string ResultString => string.Empty; public override string ResultString => string.Empty;
public override string MaxResultString => string.Empty; public override string MaxResultString => string.Empty;
/// <summary>
/// The hit result.
/// </summary>
public ManiaHitResult ManiaResult;
} }
} }

View File

@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Mania
{ {
public class ManiaRuleset : Ruleset public class ManiaRuleset : Ruleset
{ {
public override HitRenderer CreateHitRendererWith(WorkingBeatmap beatmap) => new ManiaHitRenderer(beatmap); public override HitRenderer CreateHitRendererWith(WorkingBeatmap beatmap, bool isForCurrentRuleset) => new ManiaHitRenderer(beatmap, isForCurrentRuleset);
public override IEnumerable<Mod> GetModsFor(ModType type) public override IEnumerable<Mod> GetModsFor(ModType type)
{ {

View File

@ -0,0 +1,92 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
namespace osu.Game.Rulesets.Mania.MathUtils
{
/// <summary>
/// A PRNG specified in http://heliosphan.org/fastrandom.html.
/// </summary>
internal class FastRandom
{
private const double uint_to_real = 1.0 / (uint.MaxValue + 1.0);
private const uint int_mask = 0x7FFFFFFF;
private const uint y = 842502087;
private const uint z = 3579807591;
private const uint w = 273326509;
private uint _x, _y = y, _z = z, _w = w;
public FastRandom(int seed)
{
_x = (uint)seed;
}
public FastRandom()
: this(Environment.TickCount)
{
}
/// <summary>
/// Generates a random unsigned integer within the range [<see cref="uint.MinValue"/>, <see cref="uint.MaxValue"/>).
/// </summary>
/// <returns>The random value.</returns>
public uint NextUInt()
{
uint t = _x ^ _x << 11;
_x = _y;
_y = _z;
_z = _w;
return _w = _w ^ _w >> 19 ^ t ^ t >> 8;
}
/// <summary>
/// Generates a random integer value within the range [0, <see cref="int.MaxValue"/>).
/// </summary>
/// <returns>The random value.</returns>
public int Next() => (int)(int_mask & NextUInt());
/// <summary>
/// Generates a random integer value within the range [0, <paramref name="upperBound"/>).
/// </summary>
/// <param name="upperBound">The upper bound.</param>
/// <returns>The random value.</returns>
public int Next(int upperBound) => (int)(NextDouble() * upperBound);
/// <summary>
/// Generates a random integer value within the range [<paramref name="lowerBound"/>, <paramref name="upperBound"/>).
/// </summary>
/// <param name="lowerBound">The lower bound of the range.</param>
/// <param name="upperBound">The upper bound of the range.</param>
/// <returns>The random value.</returns>
public int Next(int lowerBound, int upperBound) => (int)(lowerBound + NextDouble() * (upperBound - lowerBound));
/// <summary>
/// Generates a random double value within the range [0, 1).
/// </summary>
/// <returns>The random value.</returns>
public double NextDouble() => uint_to_real * NextUInt();
private uint bitBuffer;
private int bitIndex = 32;
/// <summary>
/// Generates a reandom boolean value. Cached such that a random value is only generated once in every 32 calls.
/// </summary>
/// <returns>The random value.</returns>
public bool NextBool()
{
if (bitIndex == 32)
{
bitBuffer = NextUInt();
bitIndex = 1;
return (bitBuffer & 1) == 1;
}
bitIndex++;
return ((bitBuffer >>= 1) & 1) == 1;
}
}
}

View File

@ -5,6 +5,8 @@ using osu.Game.Rulesets.Objects.Drawables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
using OpenTK.Graphics; using OpenTK.Graphics;
using osu.Framework.Configuration;
using OpenTK.Input;
namespace osu.Game.Rulesets.Mania.Objects.Drawables namespace osu.Game.Rulesets.Mania.Objects.Drawables
{ {
@ -14,8 +16,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
private readonly BodyPiece bodyPiece; private readonly BodyPiece bodyPiece;
private readonly NotePiece tailPiece; private readonly NotePiece tailPiece;
public DrawableHoldNote(HoldNote hitObject) public DrawableHoldNote(HoldNote hitObject, Bindable<Key> key = null)
: base(hitObject) : base(hitObject, key)
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
Height = (float)HitObject.Duration; Height = (float)HitObject.Duration;
@ -38,7 +40,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
tailPiece = new NotePiece tailPiece = new NotePiece
{ {
Anchor = Anchor.BottomCentre, Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre Origin = Anchor.TopCentre
} }
}); });
} }
@ -61,5 +63,16 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
protected override void UpdateState(ArmedState state) protected override void UpdateState(ArmedState state)
{ {
} }
protected override void Update()
{
if (Time.Current > HitObject.StartTime)
headPiece.Colour = Color4.Green;
if (Time.Current > HitObject.EndTime)
{
bodyPiece.Colour = Color4.Green;
tailPiece.Colour = Color4.Green;
}
}
} }
} }

View File

@ -2,9 +2,9 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using OpenTK.Graphics; using OpenTK.Graphics;
using OpenTK.Input;
using osu.Framework.Configuration;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Rulesets.Mania.Judgements; using osu.Game.Rulesets.Mania.Judgements;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
@ -13,32 +13,23 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
public abstract class DrawableManiaHitObject<TObject> : DrawableHitObject<ManiaHitObject, ManiaJudgement> public abstract class DrawableManiaHitObject<TObject> : DrawableHitObject<ManiaHitObject, ManiaJudgement>
where TObject : ManiaHitObject where TObject : ManiaHitObject
{ {
/// <summary>
/// The key that will trigger input for this hit object.
/// </summary>
protected Bindable<Key> Key { get; private set; } = new Bindable<Key>();
public new TObject HitObject; public new TObject HitObject;
private readonly Container glowContainer; protected DrawableManiaHitObject(TObject hitObject, Bindable<Key> key = null)
protected DrawableManiaHitObject(TObject hitObject)
: base(hitObject) : base(hitObject)
{ {
HitObject = hitObject; HitObject = hitObject;
if (key != null)
Key.BindTo(key);
RelativePositionAxes = Axes.Y; RelativePositionAxes = Axes.Y;
Y = (float)HitObject.StartTime; Y = (float)HitObject.StartTime;
Add(glowContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
Children = new[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
AlwaysPresent = true
}
}
});
} }
public override Color4 AccentColour public override Color4 AccentColour
@ -49,13 +40,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
if (base.AccentColour == value) if (base.AccentColour == value)
return; return;
base.AccentColour = value; base.AccentColour = value;
glowContainer.EdgeEffect = new EdgeEffect
{
Type = EdgeEffectType.Glow,
Radius = 5,
Colour = value
};
} }
} }

View File

@ -2,6 +2,8 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using OpenTK.Graphics; using OpenTK.Graphics;
using OpenTK.Input;
using osu.Framework.Configuration;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
@ -12,8 +14,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
{ {
private readonly NotePiece headPiece; private readonly NotePiece headPiece;
public DrawableNote(Note hitObject) public DrawableNote(Note hitObject, Bindable<Key> key = null)
: base(hitObject) : base(hitObject, key)
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
Height = 100; Height = 100;

View File

@ -19,7 +19,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
public BodyPiece() public BodyPiece()
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
Masking = true;
Children = new[] Children = new[]
{ {

View File

@ -1,6 +1,7 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>. // Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Audio;
using osu.Game.Beatmaps.Timing; using osu.Game.Beatmaps.Timing;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Rulesets.Mania.Judgements; using osu.Game.Rulesets.Mania.Judgements;
@ -22,10 +23,15 @@ namespace osu.Game.Rulesets.Mania.Objects
public double Duration { get; set; } public double Duration { get; set; }
public double EndTime => StartTime + Duration; public double EndTime => StartTime + Duration;
/// <summary>
/// The samples to be played when this hold note is released.
/// </summary>
public SampleInfoList EndSamples = new SampleInfoList();
/// <summary> /// <summary>
/// The key-release hit windows for this hold note. /// The key-release hit windows for this hold note.
/// </summary> /// </summary>
protected HitWindows ReleaseHitWindows = new HitWindows(); public HitWindows ReleaseHitWindows { get; protected set; } = new HitWindows();
public override void ApplyDefaults(TimingInfo timing, BeatmapDifficulty difficulty) public override void ApplyDefaults(TimingInfo timing, BeatmapDifficulty difficulty)
{ {

View File

@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Mania.Objects
/// <summary> /// <summary>
/// The key-press hit window for this note. /// The key-press hit window for this note.
/// </summary> /// </summary>
protected HitWindows HitWindows = new HitWindows(); public HitWindows HitWindows { get; protected set; } = new HitWindows();
public override void ApplyDefaults(TimingInfo timing, BeatmapDifficulty difficulty) public override void ApplyDefaults(TimingInfo timing, BeatmapDifficulty difficulty)
{ {

View File

@ -22,5 +22,12 @@ namespace osu.Game.Rulesets.Mania.Scoring
protected override void OnNewJudgement(ManiaJudgement judgement) protected override void OnNewJudgement(ManiaJudgement judgement)
{ {
} }
protected override void Reset()
{
base.Reset();
Health.Value = 1;
}
} }
} }

View File

@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Mania.Timing
var controlPoint = drawableControlPoints.LastOrDefault(t => t.CanContain(drawable)) ?? drawableControlPoints.FirstOrDefault(); var controlPoint = drawableControlPoints.LastOrDefault(t => t.CanContain(drawable)) ?? drawableControlPoints.FirstOrDefault();
if (controlPoint == null) if (controlPoint == null)
throw new Exception("Could not find suitable timing section to add object to."); throw new InvalidOperationException("Could not find suitable timing section to add object to.");
controlPoint.Add(drawable); controlPoint.Add(drawable);
} }

View File

@ -18,6 +18,8 @@ using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Judgements; using osu.Game.Rulesets.Mania.Judgements;
using osu.Game.Beatmaps.Timing; using osu.Game.Beatmaps.Timing;
using System;
using osu.Framework.Configuration;
namespace osu.Game.Rulesets.Mania.UI namespace osu.Game.Rulesets.Mania.UI
{ {
@ -33,7 +35,10 @@ namespace osu.Game.Rulesets.Mania.UI
private const float column_width = 45; private const float column_width = 45;
private const float special_column_width = 70; private const float special_column_width = 70;
public Key Key; /// <summary>
/// The key that will trigger input actions for this column and hit objects contained inside it.
/// </summary>
public Bindable<Key> Key = new Bindable<Key>();
private readonly Box background; private readonly Box background;
private readonly Container hitTargetBar; private readonly Container hitTargetBar;
@ -95,6 +100,12 @@ namespace osu.Game.Rulesets.Mania.UI
Name = "Hit objects", Name = "Hit objects",
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
}, },
// For column lighting, we need to capture input events before the notes
new InputTarget
{
KeyDown = onKeyDown,
KeyUp = onKeyUp
}
} }
}, },
new Container new Container
@ -178,12 +189,9 @@ namespace osu.Game.Rulesets.Mania.UI
} }
} }
public void Add(DrawableHitObject<ManiaHitObject, ManiaJudgement> hitObject) public void Add(DrawableHitObject<ManiaHitObject, ManiaJudgement> hitObject) => ControlPointContainer.Add(hitObject);
{
ControlPointContainer.Add(hitObject);
}
protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) private bool onKeyDown(InputState state, KeyDownEventArgs args)
{ {
if (args.Repeat) if (args.Repeat)
return false; return false;
@ -197,7 +205,7 @@ namespace osu.Game.Rulesets.Mania.UI
return false; return false;
} }
protected override bool OnKeyUp(InputState state, KeyUpEventArgs args) private bool onKeyUp(InputState state, KeyUpEventArgs args)
{ {
if (args.Key == Key) if (args.Key == Key)
{ {
@ -207,5 +215,24 @@ namespace osu.Game.Rulesets.Mania.UI
return false; return false;
} }
/// <summary>
/// This is a simple container which delegates various input events that have to be captured before the notes.
/// </summary>
private class InputTarget : Container
{
public Func<InputState, KeyDownEventArgs, bool> KeyDown;
public Func<InputState, KeyUpEventArgs, bool> KeyUp;
public InputTarget()
{
RelativeSizeAxes = Axes.Both;
AlwaysPresent = true;
Alpha = 0;
}
protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) => KeyDown?.Invoke(state, args) ?? false;
protected override bool OnKeyUp(InputState state, KeyUpEventArgs args) => KeyUp?.Invoke(state, args) ?? false;
}
} }
} }

View File

@ -4,6 +4,8 @@
using System; using System;
using System.Linq; using System.Linq;
using OpenTK; using OpenTK;
using OpenTK.Input;
using osu.Framework.Configuration;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Timing; using osu.Game.Beatmaps.Timing;
@ -24,8 +26,8 @@ namespace osu.Game.Rulesets.Mania.UI
{ {
public int? Columns; public int? Columns;
public ManiaHitRenderer(WorkingBeatmap beatmap) public ManiaHitRenderer(WorkingBeatmap beatmap, bool isForCurrentRuleset)
: base(beatmap) : base(beatmap, isForCurrentRuleset)
{ {
} }
@ -34,7 +36,7 @@ namespace osu.Game.Rulesets.Mania.UI
ControlPoint firstTimingChange = Beatmap.TimingInfo.ControlPoints.FirstOrDefault(t => t.TimingChange); ControlPoint firstTimingChange = Beatmap.TimingInfo.ControlPoints.FirstOrDefault(t => t.TimingChange);
if (firstTimingChange == null) if (firstTimingChange == null)
throw new Exception("The Beatmap contains no timing points!"); throw new InvalidOperationException("The Beatmap contains no timing points!");
// Generate the timing points, making non-timing changes use the previous timing change // Generate the timing points, making non-timing changes use the previous timing change
var timingChanges = Beatmap.TimingInfo.ControlPoints.Select(c => var timingChanges = Beatmap.TimingInfo.ControlPoints.Select(c =>
@ -49,7 +51,7 @@ namespace osu.Game.Rulesets.Mania.UI
return t; return t;
}); });
double lastObjectTime = (Objects.Last() as IHasEndTime)?.EndTime ?? Objects.Last().StartTime; double lastObjectTime = (Objects.LastOrDefault() as IHasEndTime)?.EndTime ?? Objects.LastOrDefault()?.StartTime ?? double.MaxValue;
// Perform some post processing of the timing changes // Perform some post processing of the timing changes
timingChanges = timingChanges timingChanges = timingChanges
@ -76,9 +78,19 @@ namespace osu.Game.Rulesets.Mania.UI
protected override DrawableHitObject<ManiaHitObject, ManiaJudgement> GetVisualRepresentation(ManiaHitObject h) protected override DrawableHitObject<ManiaHitObject, ManiaJudgement> GetVisualRepresentation(ManiaHitObject h)
{ {
var maniaPlayfield = Playfield as ManiaPlayfield;
if (maniaPlayfield == null)
return null;
Bindable<Key> key = maniaPlayfield.Columns.ElementAt(h.Column).Key;
var holdNote = h as HoldNote;
if (holdNote != null)
return new DrawableHoldNote(holdNote, key);
var note = h as Note; var note = h as Note;
if (note != null) if (note != null)
return new DrawableNote(note); return new DrawableNote(note, key);
return null; return null;
} }

View File

@ -55,7 +55,8 @@ namespace osu.Game.Rulesets.Mania.UI
} }
} }
public readonly FlowContainer<Column> Columns; private readonly FlowContainer<Column> columns;
public IEnumerable<Column> Columns => columns.Children;
private readonly ControlPointContainer barlineContainer; private readonly ControlPointContainer barlineContainer;
@ -87,7 +88,7 @@ namespace osu.Game.Rulesets.Mania.UI
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = Color4.Black Colour = Color4.Black
}, },
Columns = new FillFlowContainer<Column> columns = new FillFlowContainer<Column>
{ {
Name = "Columns", Name = "Columns",
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
@ -114,7 +115,7 @@ namespace osu.Game.Rulesets.Mania.UI
}; };
for (int i = 0; i < columnCount; i++) for (int i = 0; i < columnCount; i++)
Columns.Add(new Column(timingChanges)); columns.Add(new Column(timingChanges));
TimeSpan = time_span_default; TimeSpan = time_span_default;
} }
@ -133,17 +134,17 @@ namespace osu.Game.Rulesets.Mania.UI
// Set the special column + colour + key // Set the special column + colour + key
for (int i = 0; i < columnCount; i++) for (int i = 0; i < columnCount; i++)
{ {
Column column = Columns.Children.ElementAt(i); Column column = Columns.ElementAt(i);
column.IsSpecial = isSpecialColumn(i); column.IsSpecial = isSpecialColumn(i);
if (!column.IsSpecial) if (!column.IsSpecial)
continue; continue;
column.Key = Key.Space; column.Key.Value = Key.Space;
column.AccentColour = specialColumnColour; column.AccentColour = specialColumnColour;
} }
var nonSpecialColumns = Columns.Children.Where(c => !c.IsSpecial).ToList(); var nonSpecialColumns = Columns.Where(c => !c.IsSpecial).ToList();
// We'll set the colours of the non-special columns in a separate loop, because the non-special // We'll set the colours of the non-special columns in a separate loop, because the non-special
// column colours are mirrored across their centre and special styles mess with this // column colours are mirrored across their centre and special styles mess with this
@ -162,11 +163,11 @@ namespace osu.Game.Rulesets.Mania.UI
int keyOffset = default_keys.Length / 2 - nonSpecialColumns.Count / 2 + i; int keyOffset = default_keys.Length / 2 - nonSpecialColumns.Count / 2 + i;
if (keyOffset >= 0 && keyOffset < default_keys.Length) if (keyOffset >= 0 && keyOffset < default_keys.Length)
column.Key = default_keys[keyOffset]; column.Key.Value = default_keys[keyOffset];
else else
// There is no default key defined for this column. Let's set this to Unknown for now // There is no default key defined for this column. Let's set this to Unknown for now
// however note that this will be gone after bindings are in place // however note that this will be gone after bindings are in place
column.Key = Key.Unknown; column.Key.Value = Key.Unknown;
} }
} }
@ -189,7 +190,7 @@ namespace osu.Game.Rulesets.Mania.UI
} }
} }
public override void Add(DrawableHitObject<ManiaHitObject, ManiaJudgement> h) => Columns.Children.ElementAt(h.HitObject.Column).Add(h); public override void Add(DrawableHitObject<ManiaHitObject, ManiaJudgement> h) => Columns.ElementAt(h.HitObject.Column).Add(h);
protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) protected override bool OnKeyDown(InputState state, KeyDownEventArgs args)
{ {
@ -225,7 +226,7 @@ namespace osu.Game.Rulesets.Mania.UI
timeSpan = MathHelper.Clamp(timeSpan, time_span_min, time_span_max); timeSpan = MathHelper.Clamp(timeSpan, time_span_min, time_span_max);
barlineContainer.TimeSpan = value; barlineContainer.TimeSpan = value;
Columns.Children.ForEach(c => c.ControlPointContainer.TimeSpan = value); Columns.ForEach(c => c.ControlPointContainer.TimeSpan = value);
} }
} }

View File

@ -47,8 +47,16 @@
<Reference Include="System.Xml" /> <Reference Include="System.Xml" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Beatmaps\Patterns\Legacy\EndTimeObjectPatternGenerator.cs" />
<Compile Include="Beatmaps\Patterns\Legacy\DistanceObjectPatternGenerator.cs" />
<Compile Include="Beatmaps\Patterns\Legacy\PatternGenerator.cs" />
<Compile Include="Beatmaps\Patterns\PatternGenerator.cs" />
<Compile Include="Beatmaps\Patterns\Legacy\PatternType.cs" />
<Compile Include="Beatmaps\ManiaBeatmapConverter.cs" /> <Compile Include="Beatmaps\ManiaBeatmapConverter.cs" />
<Compile Include="Beatmaps\Patterns\Pattern.cs" />
<Compile Include="MathUtils\FastRandom.cs" />
<Compile Include="Judgements\HitWindows.cs" /> <Compile Include="Judgements\HitWindows.cs" />
<Compile Include="Judgements\ManiaHitResult.cs" />
<Compile Include="Judgements\ManiaJudgement.cs" /> <Compile Include="Judgements\ManiaJudgement.cs" />
<Compile Include="ManiaDifficultyCalculator.cs" /> <Compile Include="ManiaDifficultyCalculator.cs" />
<Compile Include="Objects\Drawables\DrawableHoldNote.cs" /> <Compile Include="Objects\Drawables\DrawableHoldNote.cs" />

View File

@ -1,15 +1,16 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>. // Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.MathUtils;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
using OpenTK; using OpenTK;
using OpenTK.Graphics; using OpenTK.Graphics;
using osu.Game.Rulesets.Osu.UI; using osu.Game.Graphics;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Allocation;
using osu.Game.Screens.Ranking;
namespace osu.Game.Rulesets.Osu.Objects.Drawables namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
@ -18,9 +19,22 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private readonly Spinner spinner; private readonly Spinner spinner;
private readonly SpinnerDisc disc; private readonly SpinnerDisc disc;
private readonly SpinnerTicks ticks;
private readonly Container mainContainer;
private readonly SpinnerBackground background; private readonly SpinnerBackground background;
private readonly Container circleContainer; private readonly Container circleContainer;
private readonly DrawableHitCircle circle; private readonly CirclePiece circle;
private readonly GlowPiece glow;
private readonly TextAwesome symbol;
private readonly Color4 baseColour = OsuColour.FromHex(@"002c3c");
private readonly Color4 fillColour = OsuColour.FromHex(@"005b7c");
private Color4 normalColour;
private Color4 completeColour;
public DrawableSpinner(Spinner s) : base(s) public DrawableSpinner(Spinner s) : base(s)
{ {
@ -29,57 +43,91 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Origin = Anchor.Centre; Origin = Anchor.Centre;
Position = s.Position; Position = s.Position;
//take up full playfield. RelativeSizeAxes = Axes.Both;
Size = new Vector2(OsuPlayfield.BASE_SIZE.X);
// we are slightly bigger than our parent, to clip the top and bottom of the circle
Height = 1.3f;
spinner = s; spinner = s;
Children = new Drawable[] Children = new Drawable[]
{ {
background = new SpinnerBackground
{
Alpha = 0,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
DiscColour = Color4.Black
},
disc = new SpinnerDisc
{
Alpha = 0,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
DiscColour = AccentColour
},
circleContainer = new Container circleContainer = new Container
{ {
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Children = new [] Children = new Drawable[]
{ {
circle = new DrawableHitCircle(s) glow = new GlowPiece(),
circle = new CirclePiece
{ {
Interactive = false,
Position = Vector2.Zero, Position = Vector2.Zero,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
} },
new RingPiece(),
symbol = new TextAwesome
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
UseFullGlyphHeight = true,
TextSize = 48,
Icon = FontAwesome.fa_asterisk,
Shadow = false,
},
} }
} },
mainContainer = new AspectContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Y,
Children = new Drawable[]
{
background = new SpinnerBackground
{
Alpha = 0.6f,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
disc = new SpinnerDisc(spinner)
{
Scale = Vector2.Zero,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
circleContainer.CreateProxy(),
ticks = new SpinnerTicks
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
}
},
}; };
background.Scale = scaleToCircle;
disc.Scale = scaleToCircle;
} }
public float Progress => MathHelper.Clamp(disc.RotationAbsolute / 360 / spinner.SpinsRequired, 0, 1);
protected override void CheckJudgement(bool userTriggered) protected override void CheckJudgement(bool userTriggered)
{ {
if (Time.Current < HitObject.StartTime) return; if (Time.Current < HitObject.StartTime) return;
disc.ScaleTo(Interpolation.ValueAt(Math.Sqrt(Progress), scaleToCircle, Vector2.One, 0, 1), 100); if (Progress >= 1 && !disc.Complete)
{
if (Progress >= 1)
disc.Complete = true; disc.Complete = true;
const float duration = 200;
disc.FadeAccent(completeColour, duration);
background.FadeAccent(completeColour, duration);
background.FadeOut(duration);
circle.FadeColour(completeColour, duration);
glow.FadeColour(completeColour, duration);
}
if (!userTriggered && Time.Current >= spinner.EndTime) if (!userTriggered && Time.Current >= spinner.EndTime)
{ {
if (Progress >= 1) if (Progress >= 1)
@ -106,26 +154,48 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
} }
} }
private Vector2 scaleToCircle => circle.Scale * circle.DrawWidth / DrawWidth * 0.95f; [BackgroundDependencyLoader]
private void load(OsuColour colours)
{
normalColour = baseColour;
public float Progress => MathHelper.Clamp(disc.RotationAbsolute / 360 / spinner.SpinsRequired, 0, 1); background.AccentColour = normalColour;
completeColour = colours.YellowLight.Opacity(0.75f);
disc.AccentColour = fillColour;
circle.Colour = colours.BlueDark;
glow.Colour = colours.BlueDark;
}
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
circle.Rotation = disc.Rotation;
ticks.Rotation = disc.Rotation;
float relativeCircleScale = spinner.Scale * circle.DrawHeight / mainContainer.DrawHeight;
disc.ScaleTo(relativeCircleScale + (1 - relativeCircleScale) * Progress, 200, EasingTypes.OutQuint);
symbol.RotateTo(disc.Rotation / 2, 500, EasingTypes.OutQuint);
}
protected override void UpdatePreemptState() protected override void UpdatePreemptState()
{ {
base.UpdatePreemptState(); base.UpdatePreemptState();
circleContainer.ScaleTo(1, 400, EasingTypes.OutElastic); circleContainer.ScaleTo(spinner.Scale * 0.3f);
circleContainer.ScaleTo(spinner.Scale, TIME_PREEMPT / 1.4f, EasingTypes.OutQuint);
background.Delay(TIME_PREEMPT - 500); disc.RotateTo(-720);
symbol.RotateTo(-720);
background.ScaleTo(scaleToCircle * 1.2f, 400, EasingTypes.OutQuint); mainContainer.ScaleTo(0);
background.FadeIn(200); mainContainer.ScaleTo(spinner.Scale * circle.DrawHeight / DrawHeight * 1.4f, TIME_PREEMPT - 150, EasingTypes.OutQuint);
background.Delay(400); mainContainer.Delay(TIME_PREEMPT - 150);
background.ScaleTo(1, 250, EasingTypes.OutQuint); mainContainer.ScaleTo(1, 500, EasingTypes.OutQuint);
disc.Delay(TIME_PREEMPT - 50);
disc.FadeIn(200);
} }
protected override void UpdateCurrentState(ArmedState state) protected override void UpdateCurrentState(ArmedState state)

View File

@ -16,7 +16,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{ {
private readonly Sprite disc; private readonly Sprite disc;
public Func<bool> Hit; public Func<bool> Hit;
public CirclePiece() public CirclePiece()

View File

@ -1,10 +1,55 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>. // Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using OpenTK.Graphics;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{ {
public class SpinnerBackground : SpinnerDisc public class SpinnerBackground : CircularContainer, IHasAccentColour
{ {
public override bool HandleInput => false; public override bool HandleInput => false;
protected Box Disc;
public Color4 AccentColour
{
get
{
return Disc.Colour;
}
set
{
Disc.Colour = value;
EdgeEffect = new EdgeEffect
{
Hollow = true,
Type = EdgeEffectType.Glow,
Radius = 40,
Colour = value,
};
}
}
public SpinnerBackground()
{
RelativeSizeAxes = Axes.Both;
Masking = true;
Children = new Drawable[]
{
Disc = new Box
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Alpha = 1,
},
};
}
} }
} }

View File

@ -2,13 +2,8 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System; using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Transforms; using osu.Framework.Graphics.Transforms;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Game.Graphics; using osu.Game.Graphics;
@ -17,104 +12,31 @@ using OpenTK.Graphics;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{ {
public class SpinnerDisc : CircularContainer public class SpinnerDisc : CircularContainer, IHasAccentColour
{ {
protected Sprite Disc; private readonly Spinner spinner;
public SRGBColour DiscColour public Color4 AccentColour
{ {
get { return Disc.Colour; } get { return background.AccentColour; }
set { Disc.Colour = value; } set { background.AccentColour = value; }
} }
private Color4 completeColour; private readonly SpinnerBackground background;
[BackgroundDependencyLoader] private const float idle_alpha = 0.2f;
private void load(OsuColour colours) private const float tracking_alpha = 0.4f;
public SpinnerDisc(Spinner s)
{ {
completeColour = colours.YellowLight.Opacity(0.8f); spinner = s;
Masking = true;
}
private class SpinnerBorder : Container
{
public SpinnerBorder()
{
Origin = Anchor.Centre;
Anchor = Anchor.Centre;
RelativeSizeAxes = Axes.Both;
layout();
}
private int lastLayoutDotCount;
private void layout()
{
int count = (int)(MathHelper.Pi * ScreenSpaceDrawQuad.Width / 9);
if (count == lastLayoutDotCount) return;
lastLayoutDotCount = count;
while (Children.Count() < count)
{
Add(new CircularContainer
{
Colour = Color4.White,
RelativePositionAxes = Axes.Both,
Masking = true,
Origin = Anchor.Centre,
Size = new Vector2(1 / ScreenSpaceDrawQuad.Width * 2000),
Children = new[]
{
new Box
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
}
}
});
}
var size = new Vector2(1 / ScreenSpaceDrawQuad.Width * 2000);
int i = 0;
foreach (var d in Children)
{
d.Size = size;
d.Position = new Vector2(
0.5f + (float)Math.Sin((float)i / count * 2 * MathHelper.Pi) / 2,
0.5f + (float)Math.Cos((float)i / count * 2 * MathHelper.Pi) / 2
);
i++;
}
}
protected override void Update()
{
base.Update();
layout();
}
}
public SpinnerDisc()
{
AlwaysReceiveInput = true; AlwaysReceiveInput = true;
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
Children = new Drawable[] Children = new Drawable[]
{ {
Disc = new Box background = new SpinnerBackground { Alpha = idle_alpha },
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Alpha = 0.2f,
},
new SpinnerBorder()
}; };
} }
@ -125,10 +47,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
set set
{ {
if (value == tracking) return; if (value == tracking) return;
tracking = value; tracking = value;
Disc.FadeTo(tracking ? 0.5f : 0.2f, 100); background.FadeTo(tracking ? tracking_alpha : idle_alpha, 100);
} }
} }
@ -139,31 +60,28 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
set set
{ {
if (value == complete) return; if (value == complete) return;
complete = value; complete = value;
Disc.FadeColour(completeColour, 200);
updateCompleteTick(); updateCompleteTick();
} }
} }
protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) protected override bool OnMouseDown(InputState state, MouseDownEventArgs args)
{ {
Tracking = true; Tracking |= state.Mouse.HasMainButtonPressed;
return base.OnMouseDown(state, args); return base.OnMouseDown(state, args);
} }
protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) protected override bool OnMouseUp(InputState state, MouseUpEventArgs args)
{ {
Tracking = false; Tracking &= state.Mouse.HasMainButtonPressed;
return base.OnMouseUp(state, args); return base.OnMouseUp(state, args);
} }
protected override bool OnMouseMove(InputState state) protected override bool OnMouseMove(InputState state)
{ {
Tracking |= state.Mouse.HasMainButtonPressed; Tracking |= state.Mouse.HasMainButtonPressed;
mousePosition = state.Mouse.Position; mousePosition = Parent.ToLocalSpace(state.Mouse.NativeState.Position);
return base.OnMouseMove(state); return base.OnMouseMove(state);
} }
@ -177,13 +95,24 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
private bool updateCompleteTick() => completeTick != (completeTick = (int)(RotationAbsolute / 360)); private bool updateCompleteTick() => completeTick != (completeTick = (int)(RotationAbsolute / 360));
private bool rotationTransferred;
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
var thisAngle = -(float)MathHelper.RadiansToDegrees(Math.Atan2(mousePosition.X - DrawSize.X / 2, mousePosition.Y - DrawSize.Y / 2)); var thisAngle = -(float)MathHelper.RadiansToDegrees(Math.Atan2(mousePosition.X - DrawSize.X / 2, mousePosition.Y - DrawSize.Y / 2));
if (tracking)
bool validAndTracking = tracking && spinner.StartTime <= Time.Current && spinner.EndTime > Time.Current;
if (validAndTracking)
{ {
if (!rotationTransferred)
{
currentRotation = Rotation * 2;
rotationTransferred = true;
}
if (thisAngle - lastAngle > 180) if (thisAngle - lastAngle > 180)
lastAngle += 360; lastAngle += 360;
else if (lastAngle - thisAngle > 180) else if (lastAngle - thisAngle > 180)
@ -192,17 +121,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
currentRotation += thisAngle - lastAngle; currentRotation += thisAngle - lastAngle;
RotationAbsolute += Math.Abs(thisAngle - lastAngle); RotationAbsolute += Math.Abs(thisAngle - lastAngle);
} }
lastAngle = thisAngle; lastAngle = thisAngle;
if (Complete && updateCompleteTick()) if (Complete && updateCompleteTick())
{ {
Disc.Flush(flushType: typeof(TransformAlpha)); background.Flush(flushType: typeof(TransformAlpha));
Disc.FadeTo(0.75f, 30, EasingTypes.OutExpo); background.FadeTo(tracking_alpha + 0.2f, 60, EasingTypes.OutExpo);
Disc.Delay(30); background.Delay(60);
Disc.FadeTo(0.5f, 250, EasingTypes.OutQuint); background.FadeTo(tracking_alpha, 250, EasingTypes.OutQuint);
} }
RotateTo(currentRotation, 100, EasingTypes.OutExpo); RotateTo(currentRotation / 2, validAndTracking ? 500 : 1500, EasingTypes.OutExpo);
} }
} }
} }

View File

@ -0,0 +1,57 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using OpenTK;
using OpenTK.Graphics;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
public class SpinnerTicks : Container
{
public SpinnerTicks()
{
Origin = Anchor.Centre;
Anchor = Anchor.Centre;
RelativeSizeAxes = Axes.Both;
const int count = 18;
for (int i = 0; i < count; i++)
{
Add(new Container
{
Colour = Color4.Black,
Alpha = 0.4f,
EdgeEffect = new EdgeEffect
{
Type = EdgeEffectType.Glow,
Radius = 10,
Colour = Color4.Gray.Opacity(0.2f),
},
RelativePositionAxes = Axes.Both,
Masking = true,
CornerRadius = 5,
Size = new Vector2(60, 10),
Origin = Anchor.Centre,
Position = new Vector2(
0.5f + (float)Math.Sin((float)i / count * 2 * MathHelper.Pi) / 2 * 0.86f,
0.5f + (float)Math.Cos((float)i / count * 2 * MathHelper.Pi) / 2 * 0.86f
),
Rotation = -(float)i / count * 360 + 90,
Children = new[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
}
}
});
}
}
}
}

View File

@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu
{ {
public class OsuRuleset : Ruleset public class OsuRuleset : Ruleset
{ {
public override HitRenderer CreateHitRendererWith(WorkingBeatmap beatmap) => new OsuHitRenderer(beatmap); public override HitRenderer CreateHitRendererWith(WorkingBeatmap beatmap, bool isForCurrentRuleset) => new OsuHitRenderer(beatmap, isForCurrentRuleset);
public override IEnumerable<BeatmapStatistic> GetBeatmapStatistics(WorkingBeatmap beatmap) => new[] public override IEnumerable<BeatmapStatistic> GetBeatmapStatistics(WorkingBeatmap beatmap) => new[]
{ {

View File

@ -91,25 +91,25 @@ namespace osu.Game.Rulesets.Osu.Replays
// Make the cursor stay at a hitObject as long as possible (mainly for autopilot). // Make the cursor stay at a hitObject as long as possible (mainly for autopilot).
if (h.StartTime - h.HitWindowFor(OsuScoreResult.Miss) > endTime + h.HitWindowFor(OsuScoreResult.Hit50) + 50) if (h.StartTime - h.HitWindowFor(OsuScoreResult.Miss) > endTime + h.HitWindowFor(OsuScoreResult.Hit50) + 50)
{ {
if (!(prev is Spinner) && h.StartTime - endTime < 1000) AddFrameToReplay(new ReplayFrame(endTime + h.HitWindowFor(OsuScoreResult.Hit50), prev.EndPosition.X, prev.EndPosition.Y, ReplayButtonState.None)); if (!(prev is Spinner) && h.StartTime - endTime < 1000) AddFrameToReplay(new ReplayFrame(endTime + h.HitWindowFor(OsuScoreResult.Hit50), prev.StackedEndPosition.X, prev.StackedEndPosition.Y, ReplayButtonState.None));
if (!(h is Spinner)) AddFrameToReplay(new ReplayFrame(h.StartTime - h.HitWindowFor(OsuScoreResult.Miss), h.Position.X, h.Position.Y, ReplayButtonState.None)); if (!(h is Spinner)) AddFrameToReplay(new ReplayFrame(h.StartTime - h.HitWindowFor(OsuScoreResult.Miss), h.StackedPosition.X, h.StackedPosition.Y, ReplayButtonState.None));
} }
else if (h.StartTime - h.HitWindowFor(OsuScoreResult.Hit50) > endTime + h.HitWindowFor(OsuScoreResult.Hit50) + 50) else if (h.StartTime - h.HitWindowFor(OsuScoreResult.Hit50) > endTime + h.HitWindowFor(OsuScoreResult.Hit50) + 50)
{ {
if (!(prev is Spinner) && h.StartTime - endTime < 1000) AddFrameToReplay(new ReplayFrame(endTime + h.HitWindowFor(OsuScoreResult.Hit50), prev.EndPosition.X, prev.EndPosition.Y, ReplayButtonState.None)); if (!(prev is Spinner) && h.StartTime - endTime < 1000) AddFrameToReplay(new ReplayFrame(endTime + h.HitWindowFor(OsuScoreResult.Hit50), prev.StackedEndPosition.X, prev.StackedEndPosition.Y, ReplayButtonState.None));
if (!(h is Spinner)) AddFrameToReplay(new ReplayFrame(h.StartTime - h.HitWindowFor(OsuScoreResult.Hit50), h.Position.X, h.Position.Y, ReplayButtonState.None)); if (!(h is Spinner)) AddFrameToReplay(new ReplayFrame(h.StartTime - h.HitWindowFor(OsuScoreResult.Hit50), h.StackedPosition.X, h.StackedPosition.Y, ReplayButtonState.None));
} }
else if (h.StartTime - h.HitWindowFor(OsuScoreResult.Hit100) > endTime + h.HitWindowFor(OsuScoreResult.Hit100) + 50) else if (h.StartTime - h.HitWindowFor(OsuScoreResult.Hit100) > endTime + h.HitWindowFor(OsuScoreResult.Hit100) + 50)
{ {
if (!(prev is Spinner) && h.StartTime - endTime < 1000) AddFrameToReplay(new ReplayFrame(endTime + h.HitWindowFor(OsuScoreResult.Hit100), prev.EndPosition.X, prev.EndPosition.Y, ReplayButtonState.None)); if (!(prev is Spinner) && h.StartTime - endTime < 1000) AddFrameToReplay(new ReplayFrame(endTime + h.HitWindowFor(OsuScoreResult.Hit100), prev.StackedEndPosition.X, prev.StackedEndPosition.Y, ReplayButtonState.None));
if (!(h is Spinner)) AddFrameToReplay(new ReplayFrame(h.StartTime - h.HitWindowFor(OsuScoreResult.Hit100), h.Position.X, h.Position.Y, ReplayButtonState.None)); if (!(h is Spinner)) AddFrameToReplay(new ReplayFrame(h.StartTime - h.HitWindowFor(OsuScoreResult.Hit100), h.StackedPosition.X, h.StackedPosition.Y, ReplayButtonState.None));
} }
} }
private void addHitObjectReplay(OsuHitObject h) private void addHitObjectReplay(OsuHitObject h)
{ {
// Default values for circles/sliders // Default values for circles/sliders
Vector2 startPosition = h.Position; Vector2 startPosition = h.StackedPosition;
EasingTypes easing = preferredEasing; EasingTypes easing = preferredEasing;
float spinnerDirection = -1; float spinnerDirection = -1;
@ -238,7 +238,7 @@ namespace osu.Game.Rulesets.Osu.Replays
// TODO: Why do we delay 1 ms if the object is a spinner? There already is KEY_UP_DELAY from hEndTime. // TODO: Why do we delay 1 ms if the object is a spinner? There already is KEY_UP_DELAY from hEndTime.
double hEndTime = ((h as IHasEndTime)?.EndTime ?? h.StartTime) + KEY_UP_DELAY; double hEndTime = ((h as IHasEndTime)?.EndTime ?? h.StartTime) + KEY_UP_DELAY;
int endDelay = h is Spinner ? 1 : 0; int endDelay = h is Spinner ? 1 : 0;
ReplayFrame endFrame = new ReplayFrame(hEndTime + endDelay, h.EndPosition.X, h.EndPosition.Y, ReplayButtonState.None); ReplayFrame endFrame = new ReplayFrame(hEndTime + endDelay, h.StackedEndPosition.X, h.StackedEndPosition.Y, ReplayButtonState.None);
// Decrement because we want the previous frame, not the next one // Decrement because we want the previous frame, not the next one
int index = FindInsertionIndex(startFrame) - 1; int index = FindInsertionIndex(startFrame) - 1;

View File

@ -18,8 +18,8 @@ namespace osu.Game.Rulesets.Osu.UI
{ {
public class OsuHitRenderer : HitRenderer<OsuHitObject, OsuJudgement> public class OsuHitRenderer : HitRenderer<OsuHitObject, OsuJudgement>
{ {
public OsuHitRenderer(WorkingBeatmap beatmap) public OsuHitRenderer(WorkingBeatmap beatmap, bool isForCurrentRuleset)
: base(beatmap) : base(beatmap, isForCurrentRuleset)
{ {
} }

View File

@ -64,6 +64,7 @@
<Compile Include="Objects\Drawables\Pieces\RingPiece.cs" /> <Compile Include="Objects\Drawables\Pieces\RingPiece.cs" />
<Compile Include="Objects\Drawables\Pieces\SliderBouncer.cs" /> <Compile Include="Objects\Drawables\Pieces\SliderBouncer.cs" />
<Compile Include="Objects\Drawables\Pieces\SpinnerDisc.cs" /> <Compile Include="Objects\Drawables\Pieces\SpinnerDisc.cs" />
<Compile Include="Objects\Drawables\Pieces\SpinnerTicks.cs" />
<Compile Include="Objects\Drawables\Pieces\TrianglesPiece.cs" /> <Compile Include="Objects\Drawables\Pieces\TrianglesPiece.cs" />
<Compile Include="Objects\Drawables\Pieces\SliderBall.cs" /> <Compile Include="Objects\Drawables\Pieces\SliderBall.cs" />
<Compile Include="Objects\Drawables\Pieces\SliderBody.cs" /> <Compile Include="Objects\Drawables\Pieces\SliderBody.cs" />
@ -103,9 +104,7 @@
<None Include="packages.config" /> <None Include="packages.config" />
</ItemGroup> </ItemGroup>
<ItemGroup /> <ItemGroup />
<ItemGroup> <ItemGroup />
<Folder Include="Replays\" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it. <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets. Other similar extension points exist, see Microsoft.Common.targets.

View File

@ -41,13 +41,13 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
protected override IEnumerable<Type> ValidConversionTypes { get; } = new[] { typeof(HitObject) }; protected override IEnumerable<Type> ValidConversionTypes { get; } = new[] { typeof(HitObject) };
protected override Beatmap<TaikoHitObject> ConvertBeatmap(Beatmap original) protected override Beatmap<TaikoHitObject> ConvertBeatmap(Beatmap original, bool isForCurrentRuleset)
{ {
// Rewrite the beatmap info to add the slider velocity multiplier // Rewrite the beatmap info to add the slider velocity multiplier
BeatmapInfo info = original.BeatmapInfo.DeepClone(); BeatmapInfo info = original.BeatmapInfo.DeepClone();
info.Difficulty.SliderMultiplier *= legacy_velocity_multiplier; info.Difficulty.SliderMultiplier *= legacy_velocity_multiplier;
Beatmap<TaikoHitObject> converted = base.ConvertBeatmap(original); Beatmap<TaikoHitObject> converted = base.ConvertBeatmap(original, isForCurrentRuleset);
// Post processing step to transform hit objects with the same start time into strong hits // Post processing step to transform hit objects with the same start time into strong hits
converted.HitObjects = converted.HitObjects.GroupBy(t => t.StartTime).Select(x => converted.HitObjects = converted.HitObjects.GroupBy(t => t.StartTime).Select(x =>

View File

@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko
{ {
public class TaikoRuleset : Ruleset public class TaikoRuleset : Ruleset
{ {
public override HitRenderer CreateHitRendererWith(WorkingBeatmap beatmap) => new TaikoHitRenderer(beatmap); public override HitRenderer CreateHitRendererWith(WorkingBeatmap beatmap, bool isForCurrentRuleset) => new TaikoHitRenderer(beatmap, isForCurrentRuleset);
public override IEnumerable<Mod> GetModsFor(ModType type) public override IEnumerable<Mod> GetModsFor(ModType type)
{ {

View File

@ -22,8 +22,8 @@ namespace osu.Game.Rulesets.Taiko.UI
{ {
public class TaikoHitRenderer : HitRenderer<TaikoHitObject, TaikoJudgement> public class TaikoHitRenderer : HitRenderer<TaikoHitObject, TaikoJudgement>
{ {
public TaikoHitRenderer(WorkingBeatmap beatmap) public TaikoHitRenderer(WorkingBeatmap beatmap, bool isForCurrentRuleset)
: base(beatmap) : base(beatmap, isForCurrentRuleset)
{ {
} }

View File

@ -6,6 +6,7 @@ using osu.Game.Beatmaps.Timing;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
namespace osu.Game.Beatmaps namespace osu.Game.Beatmaps
{ {
@ -17,6 +18,7 @@ namespace osu.Game.Beatmaps
{ {
public BeatmapInfo BeatmapInfo; public BeatmapInfo BeatmapInfo;
public TimingInfo TimingInfo = new TimingInfo(); public TimingInfo TimingInfo = new TimingInfo();
public List<BreakPeriod> Breaks = new List<BreakPeriod>();
public readonly List<Color4> ComboColors = new List<Color4> public readonly List<Color4> ComboColors = new List<Color4>
{ {
new Color4(17, 136, 170, 255), new Color4(17, 136, 170, 255),
@ -32,6 +34,11 @@ namespace osu.Game.Beatmaps
/// </summary> /// </summary>
public List<T> HitObjects; public List<T> HitObjects;
/// <summary>
/// Total amount of break time in the beatmap.
/// </summary>
public double TotalBreakTime => Breaks.Sum(b => b.Duration);
/// <summary> /// <summary>
/// Constructs a new beatmap. /// Constructs a new beatmap.
/// </summary> /// </summary>
@ -40,6 +47,7 @@ namespace osu.Game.Beatmaps
{ {
BeatmapInfo = original?.BeatmapInfo ?? BeatmapInfo; BeatmapInfo = original?.BeatmapInfo ?? BeatmapInfo;
TimingInfo = original?.TimingInfo ?? TimingInfo; TimingInfo = original?.TimingInfo ?? TimingInfo;
Breaks = original?.Breaks ?? Breaks;
ComboColors = original?.ComboColors ?? ComboColors; ComboColors = original?.ComboColors ?? ComboColors;
} }
} }

View File

@ -34,7 +34,7 @@ namespace osu.Game.Beatmaps
protected DifficultyCalculator(Beatmap beatmap) protected DifficultyCalculator(Beatmap beatmap)
{ {
Objects = CreateBeatmapConverter().Convert(beatmap).HitObjects; Objects = CreateBeatmapConverter().Convert(beatmap, true).HitObjects;
foreach (var h in Objects) foreach (var h in Objects)
h.ApplyDefaults(beatmap.TimingInfo, beatmap.BeatmapInfo.Difficulty); h.ApplyDefaults(beatmap.TimingInfo, beatmap.BeatmapInfo.Difficulty);

View File

@ -1,16 +0,0 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
namespace osu.Game.Beatmaps.Events
{
public enum EventType
{
Background = 0,
Video = 1,
Break = 2,
Colour = 3,
Sprite = 4,
Sample = 5,
Animation = 6
}
}

View File

@ -5,7 +5,6 @@ using System;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using OpenTK.Graphics; using OpenTK.Graphics;
using osu.Game.Beatmaps.Events;
using osu.Game.Beatmaps.Timing; using osu.Game.Beatmaps.Timing;
using osu.Game.Beatmaps.Legacy; using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Objects.Legacy;
@ -204,23 +203,36 @@ namespace osu.Game.Beatmaps.Formats
private void handleEvents(Beatmap beatmap, string val) private void handleEvents(Beatmap beatmap, string val)
{ {
if (val.StartsWith(@"//"))
return;
if (val.StartsWith(@" "))
return; // TODO
string[] split = val.Split(','); string[] split = val.Split(',');
EventType type; EventType type;
int intType; if (!Enum.TryParse(split[0], out type))
if (!int.TryParse(split[0], out intType)) throw new InvalidDataException($@"Unknown event type {split[0]}");
// Todo: Implement the rest
switch (type)
{ {
if (!Enum.TryParse(split[0], out type)) case EventType.Video:
throw new InvalidDataException($@"Unknown event type {split[0]}"); case EventType.Background:
string filename = split[2].Trim('"');
if (type == EventType.Background)
beatmap.BeatmapInfo.Metadata.BackgroundFile = filename;
break;
case EventType.Break:
var breakEvent = new BreakPeriod
{
StartTime = double.Parse(split[1], NumberFormatInfo.InvariantInfo),
EndTime = double.Parse(split[2], NumberFormatInfo.InvariantInfo)
};
if (!breakEvent.HasEffect)
return;
beatmap.Breaks.Add(breakEvent);
break;
} }
else
type = (EventType)intType;
// TODO: Parse and store the rest of the event
if (type == EventType.Background)
beatmap.BeatmapInfo.Metadata.BackgroundFile = split[2].Trim('"');
} }
private void handleTimingPoints(Beatmap beatmap, string val) private void handleTimingPoints(Beatmap beatmap, string val)
@ -330,6 +342,9 @@ namespace osu.Game.Beatmaps.Formats
if (string.IsNullOrEmpty(line)) if (string.IsNullOrEmpty(line))
continue; continue;
if (line.StartsWith(" ") || line.StartsWith("_") || line.StartsWith("//"))
continue;
if (line.StartsWith(@"osu file format v")) if (line.StartsWith(@"osu file format v"))
{ {
beatmap.BeatmapInfo.BeatmapVersion = int.Parse(line.Substring(17)); beatmap.BeatmapInfo.BeatmapVersion = int.Parse(line.Substring(17));
@ -390,5 +405,16 @@ namespace osu.Game.Beatmaps.Formats
Soft = 2, Soft = 2,
Drum = 3 Drum = 3
} }
internal enum EventType
{
Background = 0,
Video = 1,
Break = 2,
Colour = 3,
Sprite = 4,
Sample = 5,
Animation = 6
}
} }
} }

View File

@ -0,0 +1,33 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
namespace osu.Game.Beatmaps.Timing
{
public class BreakPeriod
{
/// <summary>
/// The minimum duration required for a break to have any effect.
/// </summary>
private const double min_break_duration = 650;
/// <summary>
/// The break start time.
/// </summary>
public double StartTime;
/// <summary>
/// The break end time.
/// </summary>
public double EndTime;
/// <summary>
/// The break duration.
/// </summary>
public double Duration => EndTime - StartTime;
/// <summary>
/// Whether the break has any effect. Breaks that are too short are culled before they are added to the beatmap.
/// </summary>
public bool HasEffect => Duration >= min_break_duration;
}
}

View File

@ -26,6 +26,7 @@ namespace osu.Game.Database
public string[] SearchableTerms => new[] public string[] SearchableTerms => new[]
{ {
Author,
Artist, Artist,
ArtistUnicode, ArtistUnicode,
Title, Title,

View File

@ -0,0 +1,171 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
namespace osu.Game.Graphics.Containers
{
/// <summary>
/// A container that can scroll to each section inside it.
/// </summary>
public class SectionsContainer : Container
{
private Drawable expandableHeader, fixedHeader, footer;
public readonly ScrollContainer ScrollContainer;
private readonly Container<Drawable> sectionsContainer;
public Drawable ExpandableHeader
{
get { return expandableHeader; }
set
{
if (value == expandableHeader) return;
if (expandableHeader != null)
Remove(expandableHeader);
expandableHeader = value;
if (value == null) return;
Add(expandableHeader);
lastKnownScroll = float.NaN;
}
}
public Drawable FixedHeader
{
get { return fixedHeader; }
set
{
if (value == fixedHeader) return;
if (fixedHeader != null)
Remove(fixedHeader);
fixedHeader = value;
if (value == null) return;
Add(fixedHeader);
lastKnownScroll = float.NaN;
}
}
public Drawable Footer
{
get { return footer; }
set
{
if (value == footer) return;
if (footer != null)
ScrollContainer.Remove(footer);
footer = value;
if (value == null) return;
footer.Anchor |= Anchor.y2;
footer.Origin |= Anchor.y2;
ScrollContainer.Add(footer);
lastKnownScroll = float.NaN;
}
}
public Bindable<Drawable> SelectedSection { get; } = new Bindable<Drawable>();
protected virtual Container<Drawable> CreateScrollContentContainer()
=> new FillFlowContainer
{
Direction = FillDirection.Vertical,
AutoSizeAxes = Axes.Both
};
private List<Drawable> sections = new List<Drawable>();
public IEnumerable<Drawable> Sections
{
get { return sections; }
set
{
foreach (var section in sections)
sectionsContainer.Remove(section);
sections = value.ToList();
if (sections.Count == 0) return;
sectionsContainer.Add(sections);
SelectedSection.Value = sections[0];
lastKnownScroll = float.NaN;
}
}
private float headerHeight, footerHeight;
private readonly MarginPadding originalSectionsMargin;
private void updateSectionsMargin()
{
if (sections.Count == 0) return;
var newMargin = originalSectionsMargin;
newMargin.Top += headerHeight;
newMargin.Bottom += footerHeight;
sectionsContainer.Margin = newMargin;
}
public SectionsContainer()
{
Add(ScrollContainer = new ScrollContainer()
{
RelativeSizeAxes = Axes.Both,
Masking = false,
Children = new Drawable[] { sectionsContainer = CreateScrollContentContainer() }
});
originalSectionsMargin = sectionsContainer.Margin;
}
private float lastKnownScroll;
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
float headerH = (ExpandableHeader?.LayoutSize.Y ?? 0) + (FixedHeader?.LayoutSize.Y ?? 0);
float footerH = Footer?.LayoutSize.Y ?? 0;
if (headerH != headerHeight || footerH != footerHeight)
{
headerHeight = headerH;
footerHeight = footerH;
updateSectionsMargin();
}
float currentScroll = Math.Max(0, ScrollContainer.Current);
if (currentScroll != lastKnownScroll)
{
lastKnownScroll = currentScroll;
if (expandableHeader != null && fixedHeader != null)
{
float offset = Math.Min(expandableHeader.LayoutSize.Y, currentScroll);
expandableHeader.Y = -offset;
fixedHeader.Y = -offset + expandableHeader.LayoutSize.Y;
}
Drawable bestMatch = null;
float minDiff = float.MaxValue;
foreach (var section in sections)
{
float diff = Math.Abs(ScrollContainer.GetChildPosInContent(section) - currentScroll);
if (diff < minDiff)
{
minDiff = diff;
bestMatch = section;
}
}
if (bestMatch != null)
SelectedSection.Value = bestMatch;
}
}
}
}

View File

@ -288,7 +288,7 @@ namespace osu.Game.Online.API
{ {
APIRequest req; APIRequest req;
while (oldQueue.TryDequeue(out req)) while (oldQueue.TryDequeue(out req))
req.Fail(new Exception(@"Disconnected from server")); req.Fail(new WebException(@"Disconnected from server"));
} }
} }

View File

@ -2,6 +2,7 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using osu.Framework.IO.Network; using osu.Framework.IO.Network;
using osu.Game.Online.Chat; using osu.Game.Online.Chat;
@ -20,10 +21,7 @@ namespace osu.Game.Online.API.Requests
protected override WebRequest CreateWebRequest() protected override WebRequest CreateWebRequest()
{ {
string channelString = string.Empty; string channelString = string.Join(",", channels.Select(x => x.Id));
foreach (Channel c in channels)
channelString += c.Id + ",";
channelString = channelString.TrimEnd(',');
var req = base.CreateWebRequest(); var req = base.CreateWebRequest();
req.AddParameter(@"channels", channelString); req.AddParameter(@"channels", channelString);

View File

@ -23,7 +23,7 @@ namespace osu.Game.Online.Chat
[JsonProperty(@"channel_id")] [JsonProperty(@"channel_id")]
public int Id; public int Id;
public readonly SortedList<Message> Messages = new SortedList<Message>((m1, m2) => m1.Id.CompareTo(m2.Id)); public readonly SortedList<Message> Messages = new SortedList<Message>(Comparer<Message>.Default);
//internal bool Joined; //internal bool Joined;

View File

@ -8,7 +8,7 @@ using osu.Game.Users;
namespace osu.Game.Online.Chat namespace osu.Game.Online.Chat
{ {
public class Message public class Message : IComparable<Message>, IEquatable<Message>
{ {
[JsonProperty(@"message_id")] [JsonProperty(@"message_id")]
public readonly long Id; public readonly long Id;
@ -42,17 +42,11 @@ namespace osu.Game.Online.Chat
Id = id; Id = id;
} }
public override bool Equals(object obj) public int CompareTo(Message other) => Id.CompareTo(other.Id);
{
var objMessage = obj as Message;
return Id == objMessage?.Id; public bool Equals(Message other) => Id == other?.Id;
}
public override int GetHashCode() public override int GetHashCode() => Id.GetHashCode();
{
return Id.GetHashCode();
}
} }
public enum TargetType public enum TargetType

View File

@ -146,7 +146,7 @@ namespace osu.Game
{ {
base.LoadComplete(); base.LoadComplete();
AddInternal(ratioContainer = new RatioAdjust base.Content.Add(ratioContainer = new RatioAdjust
{ {
Children = new Drawable[] Children = new Drawable[]
{ {

View File

@ -13,7 +13,7 @@ namespace osu.Game.Overlays.Chat
public class DrawableChannel : Container public class DrawableChannel : Container
{ {
public readonly Channel Channel; public readonly Channel Channel;
private readonly FillFlowContainer flow; private readonly FillFlowContainer<ChatLine> flow;
private readonly ScrollContainer scroll; private readonly ScrollContainer scroll;
public DrawableChannel(Channel channel) public DrawableChannel(Channel channel)
@ -29,7 +29,7 @@ namespace osu.Game.Overlays.Chat
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Children = new Drawable[] Children = new Drawable[]
{ {
flow = new FillFlowContainer flow = new FillFlowContainer<ChatLine>
{ {
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
@ -63,19 +63,18 @@ namespace osu.Game.Overlays.Chat
var displayMessages = newMessages.Skip(Math.Max(0, newMessages.Count() - Channel.MAX_HISTORY)); var displayMessages = newMessages.Skip(Math.Max(0, newMessages.Count() - Channel.MAX_HISTORY));
//up to last Channel.MAX_HISTORY messages
flow.Add(displayMessages.Select(m => new ChatLine(m)));
if (scroll.IsScrolledToEnd(10) || !flow.Children.Any()) if (scroll.IsScrolledToEnd(10) || !flow.Children.Any())
scrollToEnd(); scrollToEnd();
//up to last Channel.MAX_HISTORY messages var staleMessages = flow.Children.Where(c => c.LifetimeEnd == double.MaxValue).ToArray();
foreach (Message m in displayMessages) int count = staleMessages.Length - Channel.MAX_HISTORY;
{
var d = new ChatLine(m);
flow.Add(d);
}
while (flow.Children.Count(c => c.LifetimeEnd == double.MaxValue) > Channel.MAX_HISTORY) for (int i = 0; i < count; i++)
{ {
var d = flow.Children.First(c => c.LifetimeEnd == double.MaxValue); var d = staleMessages[i];
if (!scroll.IsScrolledToEnd(10)) if (!scroll.IsScrolledToEnd(10))
scroll.OffsetScrollPosition(-d.DrawHeight); scroll.OffsetScrollPosition(-d.DrawHeight);
d.Expire(); d.Expire();

View File

@ -206,18 +206,12 @@ namespace osu.Game.Overlays
private long? lastMessageId; private long? lastMessageId;
private List<Channel> careChannels; private readonly List<Channel> careChannels = new List<Channel>();
private readonly List<DrawableChannel> loadedChannels = new List<DrawableChannel>(); private readonly List<DrawableChannel> loadedChannels = new List<DrawableChannel>();
private void initializeChannels() private void initializeChannels()
{ {
currentChannelContainer.Clear();
loadedChannels.Clear();
careChannels = new List<Channel>();
SpriteText loading; SpriteText loading;
Add(loading = new OsuSpriteText Add(loading = new OsuSpriteText
{ {
@ -232,11 +226,10 @@ namespace osu.Game.Overlays
ListChannelsRequest req = new ListChannelsRequest(); ListChannelsRequest req = new ListChannelsRequest();
req.Success += delegate (List<Channel> channels) req.Success += delegate (List<Channel> channels)
{ {
Debug.Assert(careChannels.Count == 0);
Scheduler.Add(delegate Scheduler.Add(delegate
{ {
loading.FadeOut(100); loading.FadeOut(100);
loading.Expire();
addChannel(channels.Find(c => c.Name == @"#lazer")); addChannel(channels.Find(c => c.Name == @"#lazer"));
addChannel(channels.Find(c => c.Name == @"#osu")); addChannel(channels.Find(c => c.Name == @"#osu"));
@ -330,11 +323,8 @@ namespace osu.Game.Overlays
fetchReq = new GetMessagesRequest(careChannels, lastMessageId); fetchReq = new GetMessagesRequest(careChannels, lastMessageId);
fetchReq.Success += delegate (List<Message> messages) fetchReq.Success += delegate (List<Message> messages)
{ {
var ids = messages.Where(m => m.TargetType == TargetType.Channel).Select(m => m.TargetId).Distinct(); foreach (var group in messages.Where(m => m.TargetType == TargetType.Channel).GroupBy(m => m.TargetId))
careChannels.Find(c => c.Id == group.Key)?.AddNewMessages(group.ToArray());
//batch messages per channel.
foreach (var id in ids)
careChannels.Find(c => c.Id == id)?.AddNewMessages(messages.Where(m => m.TargetId == id).ToArray());
lastMessageId = messages.LastOrDefault()?.Id ?? lastMessageId; lastMessageId = messages.LastOrDefault()?.Id ?? lastMessageId;

View File

@ -16,6 +16,7 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using System; using System;
using System.Linq; using System.Linq;
using osu.Game.Graphics;
namespace osu.Game.Overlays.Mods namespace osu.Game.Overlays.Mods
{ {
@ -23,7 +24,7 @@ namespace osu.Game.Overlays.Mods
/// <summary> /// <summary>
/// Represents a clickable button which can cycle through one of more mods. /// Represents a clickable button which can cycle through one of more mods.
/// </summary> /// </summary>
public class ModButton : ModButtonEmpty public class ModButton : ModButtonEmpty, IHasTooltip
{ {
private ModIcon foregroundIcon; private ModIcon foregroundIcon;
private readonly SpriteText text; private readonly SpriteText text;
@ -32,6 +33,8 @@ namespace osu.Game.Overlays.Mods
public Action<Mod> Action; // Passed the selected mod or null if none public Action<Mod> Action; // Passed the selected mod or null if none
public string TooltipText => (SelectedMod?.Description ?? Mods.FirstOrDefault()?.Description) ?? string.Empty;
private int _selectedIndex = -1; private int _selectedIndex = -1;
private int selectedIndex private int selectedIndex
{ {

View File

@ -7,10 +7,10 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using OpenTK;
using OpenTK.Graphics; using OpenTK.Graphics;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Framework.Graphics.Sprites;
using System.Collections.Generic;
namespace osu.Game.Overlays.Music namespace osu.Game.Overlays.Music
{ {
@ -19,9 +19,13 @@ namespace osu.Game.Overlays.Music
private const float fade_duration = 100; private const float fade_duration = 100;
private Color4 hoverColour; private Color4 hoverColour;
private Color4 artistColour;
private TextAwesome handle; private TextAwesome handle;
private OsuSpriteText title; private Paragraph text;
private IEnumerable<SpriteText> titleSprites;
private UnicodeBindableString titleBind;
private UnicodeBindableString artistBind;
public readonly BeatmapSetInfo BeatmapSetInfo; public readonly BeatmapSetInfo BeatmapSetInfo;
@ -37,7 +41,8 @@ namespace osu.Game.Overlays.Music
selected = value; selected = value;
Flush(true); Flush(true);
title.FadeColour(Selected ? hoverColour : Color4.White, fade_duration); foreach (SpriteText s in titleSprites)
s.FadeColour(Selected ? hoverColour : Color4.White, fade_duration);
} }
} }
@ -53,8 +58,10 @@ namespace osu.Game.Overlays.Music
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours, LocalisationEngine localisation) private void load(OsuColour colours, LocalisationEngine localisation)
{ {
BeatmapMetadata metadata = BeatmapSetInfo.Metadata; hoverColour = colours.Yellow;
artistColour = colours.Gray9;
var metadata = BeatmapSetInfo.Metadata;
FilterTerms = metadata.SearchableTerms; FilterTerms = metadata.SearchableTerms;
Children = new Drawable[] Children = new Drawable[]
@ -70,33 +77,40 @@ namespace osu.Game.Overlays.Music
Margin = new MarginPadding { Left = 5 }, Margin = new MarginPadding { Left = 5 },
Padding = new MarginPadding { Top = 2 }, Padding = new MarginPadding { Top = 2 },
}, },
new FillFlowContainer<OsuSpriteText> text = new Paragraph
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Left = 20 }, Padding = new MarginPadding { Left = 20 },
Spacing = new Vector2(5f, 0f), ContentIndent = 10f,
Children = new []
{
title = new OsuSpriteText
{
TextSize = 16,
Font = @"Exo2.0-Regular",
Current = localisation.GetUnicodePreference(metadata.TitleUnicode, metadata.Title),
},
new OsuSpriteText
{
TextSize = 14,
Font = @"Exo2.0-Bold",
Colour = colours.Gray9,
Padding = new MarginPadding { Top = 1 },
Current = localisation.GetUnicodePreference(metadata.ArtistUnicode, metadata.Artist),
}
}
}, },
}; };
hoverColour = colours.Yellow; titleBind = localisation.GetUnicodePreference(metadata.TitleUnicode, metadata.Title);
artistBind = localisation.GetUnicodePreference(metadata.ArtistUnicode, metadata.Artist);
artistBind.ValueChanged += newText => recreateText();
artistBind.TriggerChange();
}
private void recreateText()
{
text.Clear();
//space after the title to put a space between the title and artist
titleSprites = text.AddText(titleBind.Value + @" ", sprite =>
{
sprite.TextSize = 16;
sprite.Font = @"Exo2.0-Regular";
});
text.AddText(artistBind.Value, sprite =>
{
sprite.TextSize = 14;
sprite.Font = @"Exo2.0-Bold";
sprite.Colour = artistColour;
sprite.Padding = new MarginPadding { Top = 1 };
});
} }
protected override bool OnHover(Framework.Input.InputState state) protected override bool OnHover(Framework.Input.InputState state)

View File

@ -76,6 +76,7 @@ namespace osu.Game.Overlays
{ {
TextSize = 24, TextSize = 24,
Font = @"Exo2.0-Light", Font = @"Exo2.0-Light",
Padding = new MarginPadding { Left = 10, Right = 10 },
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.BottomCentre, Origin = Anchor.BottomCentre,
}, },
@ -116,7 +117,7 @@ namespace osu.Game.Overlays
private void load(FrameworkConfigManager frameworkConfig) private void load(FrameworkConfigManager frameworkConfig)
{ {
trackSetting(frameworkConfig.GetBindable<FrameSync>(FrameworkSetting.FrameSync), v => display(v, "Frame Limiter", v.GetDescription(), "Ctrl+F7")); trackSetting(frameworkConfig.GetBindable<FrameSync>(FrameworkSetting.FrameSync), v => display(v, "Frame Limiter", v.GetDescription(), "Ctrl+F7"));
trackSetting(frameworkConfig.GetBindable<string>(FrameworkSetting.AudioDevice), v => display(v, "Audio Device", v, v)); trackSetting(frameworkConfig.GetBindable<string>(FrameworkSetting.AudioDevice), v => display(v, "Audio Device", string.IsNullOrEmpty(v) ? "Default" : v, v));
trackSetting(frameworkConfig.GetBindable<bool>(FrameworkSetting.ShowLogOverlay), v => display(v, "Debug Logs", v ? "visible" : "hidden", "Ctrl+F10")); trackSetting(frameworkConfig.GetBindable<bool>(FrameworkSetting.ShowLogOverlay), v => display(v, "Debug Logs", v ? "visible" : "hidden", "Ctrl+F10"));
Action displayResolution = delegate { display(null, "Screen Resolution", frameworkConfig.Get<int>(FrameworkSetting.Width) + "x" + frameworkConfig.Get<int>(FrameworkSetting.Height)); }; Action displayResolution = delegate { display(null, "Screen Resolution", frameworkConfig.Get<int>(FrameworkSetting.Width) + "x" + frameworkConfig.Get<int>(FrameworkSetting.Height)); };

View File

@ -22,7 +22,7 @@ namespace osu.Game.Overlays.Settings.Sections.Debug
new SettingsEnumDropdown<GCLatencyMode> new SettingsEnumDropdown<GCLatencyMode>
{ {
LabelText = "Active mode", LabelText = "Active mode",
Bindable = config.GetBindable<GCLatencyMode>(FrameworkDebugConfig.ActiveGCMode) Bindable = config.GetBindable<GCLatencyMode>(DebugSetting.ActiveGCMode)
}, },
new OsuButton new OsuButton
{ {

View File

@ -12,14 +12,19 @@ namespace osu.Game.Overlays.Settings.Sections.Debug
protected override string Header => "General"; protected override string Header => "General";
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(FrameworkDebugConfigManager config) private void load(FrameworkDebugConfigManager config, FrameworkConfigManager frameworkConfig)
{ {
Children = new Drawable[] Children = new Drawable[]
{ {
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = "Bypass caching", LabelText = "Bypass caching",
Bindable = config.GetBindable<bool>(FrameworkDebugConfig.BypassCaching) Bindable = config.GetBindable<bool>(DebugSetting.BypassCaching)
},
new SettingsCheckbox
{
LabelText = "Debug logs",
Bindable = frameworkConfig.GetBindable<bool>(FrameworkSetting.ShowLogOverlay)
} }
}; };
} }

View File

@ -1,34 +1,16 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>. // Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using OpenTK.Graphics;
namespace osu.Game.Overlays.Settings namespace osu.Game.Overlays.Settings
{ {
public class SettingsHeader : Container public class SettingsHeader : Container
{ {
public SearchTextBox SearchTextBox;
private Box background;
private readonly Func<float> currentScrollOffset;
public Action Exit;
/// <param name="currentScrollOffset">A reference to the current scroll position of the ScrollContainer we are contained within.</param>
public SettingsHeader(Func<float> currentScrollOffset)
{
this.currentScrollOffset = currentScrollOffset;
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours)
{ {
@ -37,11 +19,6 @@ namespace osu.Game.Overlays.Settings
Children = new Drawable[] Children = new Drawable[]
{ {
background = new Box
{
Colour = Color4.Black,
RelativeSizeAxes = Axes.Both,
},
new FillFlowContainer new FillFlowContainer
{ {
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
@ -53,7 +30,8 @@ namespace osu.Game.Overlays.Settings
{ {
Text = "settings", Text = "settings",
TextSize = 40, TextSize = 40,
Margin = new MarginPadding { Margin = new MarginPadding
{
Left = SettingsOverlay.CONTENT_MARGINS, Left = SettingsOverlay.CONTENT_MARGINS,
Top = Toolbar.Toolbar.TOOLTIP_HEIGHT Top = Toolbar.Toolbar.TOOLTIP_HEIGHT
}, },
@ -63,45 +41,15 @@ namespace osu.Game.Overlays.Settings
Colour = colours.Pink, Colour = colours.Pink,
Text = "Change the way osu! behaves", Text = "Change the way osu! behaves",
TextSize = 18, TextSize = 18,
Margin = new MarginPadding { Margin = new MarginPadding
{
Left = SettingsOverlay.CONTENT_MARGINS, Left = SettingsOverlay.CONTENT_MARGINS,
Bottom = 30 Bottom = 30
}, },
}, },
SearchTextBox = new SearchTextBox
{
RelativeSizeAxes = Axes.X,
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
Width = 0.95f,
Margin = new MarginPadding {
Top = 20,
Bottom = 20
},
Exit = () => Exit(),
},
} }
} }
}; };
} }
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
// the point at which we will start anchoring to the top.
float anchorOffset = SearchTextBox.Y;
float scrollPosition = currentScrollOffset();
// we want to anchor the search field to the top of the screen when scrolling.
Margin = new MarginPadding { Top = Math.Max(0, scrollPosition - anchorOffset) };
// we don't want the header to scroll when scrolling beyond the upper extent.
Y = Math.Min(0, scrollPosition);
// we get darker as scroll progresses
background.Alpha = Math.Min(1, scrollPosition / anchorOffset) * 0.5f;
}
} }
} }

View File

@ -21,7 +21,7 @@ namespace osu.Game.Overlays.Settings
private readonly Box backgroundBox; private readonly Box backgroundBox;
private readonly Box selectionIndicator; private readonly Box selectionIndicator;
private readonly Container text; private readonly Container text;
public Action Action; public Action<SettingsSection> Action;
private SettingsSection section; private SettingsSection section;
public SettingsSection Section public SettingsSection Section
@ -75,6 +75,7 @@ namespace osu.Game.Overlays.Settings
{ {
Width = Sidebar.DEFAULT_WIDTH, Width = Sidebar.DEFAULT_WIDTH,
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
Colour = OsuColour.Gray(0.6f),
Children = new[] Children = new[]
{ {
headerText = new OsuSpriteText headerText = new OsuSpriteText
@ -110,7 +111,7 @@ namespace osu.Game.Overlays.Settings
protected override bool OnClick(InputState state) protected override bool OnClick(InputState state)
{ {
Action?.Invoke(); Action?.Invoke(section);
backgroundBox.FlashColour(Color4.White, 400); backgroundBox.FlashColour(Color4.White, 400);
return true; return true;
} }

View File

@ -7,10 +7,11 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Overlays.Settings;
using System;
using osu.Game.Overlays.Settings.Sections;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Settings;
using osu.Game.Overlays.Settings.Sections;
namespace osu.Game.Overlays namespace osu.Game.Overlays
{ {
@ -26,18 +27,13 @@ namespace osu.Game.Overlays
private const float sidebar_padding = 10; private const float sidebar_padding = 10;
private ScrollContainer scrollContainer;
private Sidebar sidebar; private Sidebar sidebar;
private SidebarButton[] sidebarButtons; private SidebarButton[] sidebarButtons;
private SettingsSection[] sections; private SidebarButton selectedSidebarButton;
private SettingsHeader header; private SettingsSectionsContainer sectionsContainer;
private SettingsFooter footer; private SearchTextBox searchTextBox;
private SearchContainer searchContainer;
private float lastKnownScroll;
public SettingsOverlay() public SettingsOverlay()
{ {
@ -48,7 +44,7 @@ namespace osu.Game.Overlays
[BackgroundDependencyLoader(permitNulls: true)] [BackgroundDependencyLoader(permitNulls: true)]
private void load(OsuGame game) private void load(OsuGame game)
{ {
sections = new SettingsSection[] var sections = new SettingsSection[]
{ {
new GeneralSection(), new GeneralSection(),
new GraphicsSection(), new GraphicsSection(),
@ -68,27 +64,27 @@ namespace osu.Game.Overlays
Colour = Color4.Black, Colour = Color4.Black,
Alpha = 0.6f, Alpha = 0.6f,
}, },
scrollContainer = new ScrollContainer sectionsContainer = new SettingsSectionsContainer
{ {
ScrollDraggerVisible = false,
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
Width = width, Width = width,
Margin = new MarginPadding { Left = SIDEBAR_WIDTH }, Margin = new MarginPadding { Left = SIDEBAR_WIDTH },
Children = new Drawable[] ExpandableHeader = new SettingsHeader(),
FixedHeader = searchTextBox = new SearchTextBox
{ {
searchContainer = new SearchContainer RelativeSizeAxes = Axes.X,
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
Width = 0.95f,
Margin = new MarginPadding
{ {
AutoSizeAxes = Axes.Y, Top = 20,
RelativeSizeAxes = Axes.X, Bottom = 20
Direction = FillDirection.Vertical,
Children = sections,
}, },
footer = new SettingsFooter(), Exit = Hide,
header = new SettingsHeader(() => scrollContainer.Current) },
{ Sections = sections,
Exit = Hide, Footer = new SettingsFooter()
},
}
}, },
sidebar = new Sidebar sidebar = new Sidebar
{ {
@ -96,84 +92,89 @@ namespace osu.Game.Overlays
Children = sidebarButtons = sections.Select(section => Children = sidebarButtons = sections.Select(section =>
new SidebarButton new SidebarButton
{ {
Selected = sections[0] == section,
Section = section, Section = section,
Action = () => scrollContainer.ScrollIntoView(section), Action = sectionsContainer.ScrollContainer.ScrollIntoView,
} }
).ToArray() ).ToArray()
} }
}; };
header.SearchTextBox.Current.ValueChanged += newValue => searchContainer.SearchTerm = newValue; selectedSidebarButton = sidebarButtons[0];
selectedSidebarButton.Selected = true;
scrollContainer.Padding = new MarginPadding { Top = game?.Toolbar.DrawHeight ?? 0 }; sectionsContainer.SelectedSection.ValueChanged += section =>
}
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
//we need to update these manually because we can't put the SettingsHeader inside the SearchContainer (due to its anchoring).
searchContainer.Y = header.DrawHeight;
footer.Y = searchContainer.Y + searchContainer.DrawHeight;
}
protected override void Update()
{
base.Update();
float currentScroll = scrollContainer.Current;
if (currentScroll != lastKnownScroll)
{ {
lastKnownScroll = currentScroll; selectedSidebarButton.Selected = false;
selectedSidebarButton = sidebarButtons.Single(b => b.Section == section);
selectedSidebarButton.Selected = true;
};
SettingsSection bestCandidate = null; searchTextBox.Current.ValueChanged += newValue => sectionsContainer.SearchContainer.SearchTerm = newValue;
float bestDistance = float.MaxValue;
foreach (SettingsSection section in sections) sectionsContainer.Padding = new MarginPadding { Top = game?.Toolbar.DrawHeight ?? 0 };
{
float distance = Math.Abs(scrollContainer.GetChildPosInContent(section) - currentScroll);
if (distance < bestDistance)
{
bestDistance = distance;
bestCandidate = section;
}
}
var previous = sidebarButtons.SingleOrDefault(sb => sb.Selected);
var next = sidebarButtons.SingleOrDefault(sb => sb.Section == bestCandidate);
if (previous != null) previous.Selected = false;
if (next != null) next.Selected = true;
}
} }
protected override void PopIn() protected override void PopIn()
{ {
base.PopIn(); base.PopIn();
scrollContainer.MoveToX(0, TRANSITION_LENGTH, EasingTypes.OutQuint); sectionsContainer.MoveToX(0, TRANSITION_LENGTH, EasingTypes.OutQuint);
sidebar.MoveToX(0, TRANSITION_LENGTH, EasingTypes.OutQuint); sidebar.MoveToX(0, TRANSITION_LENGTH, EasingTypes.OutQuint);
FadeTo(1, TRANSITION_LENGTH / 2); FadeTo(1, TRANSITION_LENGTH / 2);
header.SearchTextBox.HoldFocus = true; searchTextBox.HoldFocus = true;
} }
protected override void PopOut() protected override void PopOut()
{ {
base.PopOut(); base.PopOut();
scrollContainer.MoveToX(-width, TRANSITION_LENGTH, EasingTypes.OutQuint); sectionsContainer.MoveToX(-width, TRANSITION_LENGTH, EasingTypes.OutQuint);
sidebar.MoveToX(-SIDEBAR_WIDTH, TRANSITION_LENGTH, EasingTypes.OutQuint); sidebar.MoveToX(-SIDEBAR_WIDTH, TRANSITION_LENGTH, EasingTypes.OutQuint);
FadeTo(0, TRANSITION_LENGTH / 2); FadeTo(0, TRANSITION_LENGTH / 2);
header.SearchTextBox.HoldFocus = false; searchTextBox.HoldFocus = false;
header.SearchTextBox.TriggerFocusLost(); searchTextBox.TriggerFocusLost();
} }
protected override bool OnFocus(InputState state) protected override bool OnFocus(InputState state)
{ {
header.SearchTextBox.TriggerFocus(state); searchTextBox.TriggerFocus(state);
return false; return false;
} }
private class SettingsSectionsContainer : SectionsContainer
{
public SearchContainer SearchContainer;
private readonly Box headerBackground;
protected override Container<Drawable> CreateScrollContentContainer()
=> SearchContainer = new SearchContainer
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Direction = FillDirection.Vertical,
};
public SettingsSectionsContainer()
{
ScrollContainer.ScrollDraggerVisible = false;
Add(headerBackground = new Box
{
Colour = Color4.Black,
RelativeSizeAxes = Axes.X
});
}
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
// no null check because the usage of this class is strict
headerBackground.Height = ExpandableHeader.LayoutSize.Y + FixedHeader.LayoutSize.Y;
headerBackground.Y = ExpandableHeader.Y;
headerBackground.Alpha = -ExpandableHeader.Y / ExpandableHeader.LayoutSize.Y * 0.5f;
}
}
} }
} }

View File

@ -26,19 +26,21 @@ namespace osu.Game.Rulesets.Beatmaps
/// Converts a Beatmap using this Beatmap Converter. /// Converts a Beatmap using this Beatmap Converter.
/// </summary> /// </summary>
/// <param name="original">The un-converted Beatmap.</param> /// <param name="original">The un-converted Beatmap.</param>
/// <param name="isForCurrentRuleset">Whether to assume the beatmap is for the current ruleset.</param>
/// <returns>The converted Beatmap.</returns> /// <returns>The converted Beatmap.</returns>
public Beatmap<T> Convert(Beatmap original) public Beatmap<T> Convert(Beatmap original, bool isForCurrentRuleset)
{ {
// We always operate on a clone of the original beatmap, to not modify it game-wide // We always operate on a clone of the original beatmap, to not modify it game-wide
return ConvertBeatmap(new Beatmap(original)); return ConvertBeatmap(new Beatmap(original), isForCurrentRuleset);
} }
/// <summary> /// <summary>
/// Performs the conversion of a Beatmap using this Beatmap Converter. /// Performs the conversion of a Beatmap using this Beatmap Converter.
/// </summary> /// </summary>
/// <param name="original">The un-converted Beatmap.</param> /// <param name="original">The un-converted Beatmap.</param>
/// <param name="isForCurrentRuleset">Whether to assume the beatmap is for the current ruleset.</param>
/// <returns>The converted Beatmap.</returns> /// <returns>The converted Beatmap.</returns>
protected virtual Beatmap<T> ConvertBeatmap(Beatmap original) protected virtual Beatmap<T> ConvertBeatmap(Beatmap original, bool isForCurrentRuleset)
{ {
return new Beatmap<T> return new Beatmap<T>
{ {

View File

@ -43,5 +43,10 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch
EndTime = endTime EndTime = endTime
}; };
} }
protected override HitObject CreateHold(Vector2 position, bool newCombo, double endTime)
{
return null;
}
} }
} }

View File

@ -8,6 +8,7 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Formats;
using osu.Game.Audio; using osu.Game.Audio;
using System.Linq;
namespace osu.Game.Rulesets.Objects.Legacy namespace osu.Game.Rulesets.Objects.Legacy
{ {
@ -26,7 +27,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
var soundType = (LegacySoundType)int.Parse(split[4]); var soundType = (LegacySoundType)int.Parse(split[4]);
var bankInfo = new SampleBankInfo(); var bankInfo = new SampleBankInfo();
HitObject result; HitObject result = null;
if ((type & ConvertHitObjectType.Circle) > 0) if ((type & ConvertHitObjectType.Circle) > 0)
{ {
@ -140,17 +141,20 @@ namespace osu.Game.Rulesets.Objects.Legacy
{ {
// Note: Hold is generated by BMS converts // Note: Hold is generated by BMS converts
// Todo: Apparently end time is determined by samples?? double endTime = Convert.ToDouble(split[2], CultureInfo.InvariantCulture);
// Shouldn't need implementation until mania
result = new ConvertHold if (split.Length > 5 && !string.IsNullOrEmpty(split[5]))
{ {
Position = new Vector2(int.Parse(split[0]), int.Parse(split[1])), string[] ss = split[5].Split(':');
NewCombo = combo endTime = Convert.ToDouble(ss[0], CultureInfo.InvariantCulture);
}; readCustomSampleBanks(string.Join(":", ss.Skip(1)), bankInfo);
}
result = CreateHold(new Vector2(int.Parse(split[0]), int.Parse(split[1])), combo, endTime);
} }
else
throw new InvalidOperationException($@"Unknown hit object type {type}"); if (result == null)
throw new InvalidOperationException($@"Unknown hit object type {type}.");
result.StartTime = Convert.ToDouble(split[2], CultureInfo.InvariantCulture); result.StartTime = Convert.ToDouble(split[2], CultureInfo.InvariantCulture);
result.Samples = convertSoundType(soundType, bankInfo); result.Samples = convertSoundType(soundType, bankInfo);
@ -214,6 +218,14 @@ namespace osu.Game.Rulesets.Objects.Legacy
/// <returns>The hit object.</returns> /// <returns>The hit object.</returns>
protected abstract HitObject CreateSpinner(Vector2 position, double endTime); protected abstract HitObject CreateSpinner(Vector2 position, double endTime);
/// <summary>
/// Creates a legacy Hold-type hit object.
/// </summary>
/// <param name="position">The position of the hit object.</param>
/// <param name="newCombo">Whether the hit object creates a new combo.</param>
/// <param name="endTime">The hold end time.</param>
protected abstract HitObject CreateHold(Vector2 position, bool newCombo, double endTime);
private SampleInfoList convertSoundType(LegacySoundType type, SampleBankInfo bankInfo) private SampleInfoList convertSoundType(LegacySoundType type, SampleBankInfo bankInfo)
{ {
var soundTypes = new SampleInfoList var soundTypes = new SampleInfoList

View File

@ -1,22 +0,0 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using OpenTK;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Objects.Legacy
{
/// <summary>
/// Legacy Hold-type, used for parsing "specials" in beatmaps.
/// </summary>
internal sealed class ConvertHold : HitObject, IHasPosition, IHasCombo, IHasHold
{
public Vector2 Position { get; set; }
public float X => Position.X;
public float Y => Position.Y;
public bool NewCombo { get; set; }
}
}

View File

@ -44,5 +44,14 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania
EndTime = endTime EndTime = endTime
}; };
} }
protected override HitObject CreateHold(Vector2 position, bool newCombo, double endTime)
{
return new ConvertHold
{
X = position.X,
EndTime = endTime
};
}
} }
} }

View File

@ -0,0 +1,16 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Objects.Legacy.Mania
{
internal sealed class ConvertHold : HitObject, IHasXPosition, IHasEndTime
{
public float X { get; set; }
public double EndTime { get; set; }
public double Duration => EndTime - StartTime;
}
}

View File

@ -44,5 +44,10 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu
EndTime = endTime EndTime = endTime
}; };
} }
protected override HitObject CreateHold(Vector2 position, bool newCombo, double endTime)
{
return null;
}
} }
} }

View File

@ -41,5 +41,10 @@ namespace osu.Game.Rulesets.Objects.Legacy.Taiko
EndTime = endTime EndTime = endTime
}; };
} }
protected override HitObject CreateHold(Vector2 position, bool newCombo, double endTime)
{
return null;
}
} }
} }

View File

@ -8,5 +8,9 @@ namespace osu.Game.Rulesets.Objects.Types
/// </summary> /// </summary>
public interface IHasHold public interface IHasHold
{ {
/// <summary>
/// The time at which the hold ends.
/// </summary>
double EndTime { get; }
} }
} }

View File

@ -18,12 +18,13 @@ namespace osu.Game.Rulesets
public abstract IEnumerable<Mod> GetModsFor(ModType type); public abstract IEnumerable<Mod> GetModsFor(ModType type);
/// <summary> /// <summary>
/// Attempt to create a HitRenderer for the provided beatmap. /// Attempt to create a hit renderer for a beatmap
/// </summary> /// </summary>
/// <param name="beatmap"></param> /// <param name="beatmap">The beatmap to create the hit renderer for.</param>
/// <param name="isForCurrentRuleset">Whether the hit renderer should assume the beatmap is for the current ruleset.</param>
/// <exception cref="BeatmapInvalidForRulesetException">Unable to successfully load the beatmap to be usable with this ruleset.</exception> /// <exception cref="BeatmapInvalidForRulesetException">Unable to successfully load the beatmap to be usable with this ruleset.</exception>
/// <returns></returns> /// <returns></returns>
public abstract HitRenderer CreateHitRendererWith(WorkingBeatmap beatmap); public abstract HitRenderer CreateHitRendererWith(WorkingBeatmap beatmap, bool isForCurrentRuleset);
public abstract DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap); public abstract DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap);

View File

@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.UI
/// </summary> /// </summary>
protected abstract bool AllObjectsJudged { get; } protected abstract bool AllObjectsJudged { get; }
protected HitRenderer() internal HitRenderer()
{ {
KeyConversionInputManager = CreateKeyConversionInputManager(); KeyConversionInputManager = CreateKeyConversionInputManager();
KeyConversionInputManager.RelativeSizeAxes = Axes.Both; KeyConversionInputManager.RelativeSizeAxes = Axes.Both;
@ -120,7 +120,12 @@ namespace osu.Game.Rulesets.UI
/// </summary> /// </summary>
public Beatmap<TObject> Beatmap; public Beatmap<TObject> Beatmap;
protected HitRenderer(WorkingBeatmap beatmap) /// <summary>
/// Creates a hit renderer for a beatmap.
/// </summary>
/// <param name="beatmap">The beatmap to create the hit renderer for.</param>
/// <param name="isForCurrentRuleset">Whether to assume the beatmap is for the current ruleset.</param>
internal HitRenderer(WorkingBeatmap beatmap, bool isForCurrentRuleset)
{ {
Debug.Assert(beatmap != null, "HitRenderer initialized with a null beatmap."); Debug.Assert(beatmap != null, "HitRenderer initialized with a null beatmap.");
@ -134,7 +139,7 @@ namespace osu.Game.Rulesets.UI
throw new BeatmapInvalidForRulesetException($"{nameof(Beatmap)} can't be converted for the current ruleset."); throw new BeatmapInvalidForRulesetException($"{nameof(Beatmap)} can't be converted for the current ruleset.");
// Convert the beatmap // Convert the beatmap
Beatmap = converter.Convert(beatmap.Beatmap); Beatmap = converter.Convert(beatmap.Beatmap, isForCurrentRuleset);
// Apply defaults // Apply defaults
foreach (var h in Beatmap.HitObjects) foreach (var h in Beatmap.HitObjects)
@ -201,8 +206,13 @@ namespace osu.Game.Rulesets.UI
private readonly List<DrawableHitObject<TObject, TJudgement>> drawableObjects = new List<DrawableHitObject<TObject, TJudgement>>(); private readonly List<DrawableHitObject<TObject, TJudgement>> drawableObjects = new List<DrawableHitObject<TObject, TJudgement>>();
protected HitRenderer(WorkingBeatmap beatmap) /// <summary>
: base(beatmap) /// Creates a hit renderer for a beatmap.
/// </summary>
/// <param name="beatmap">The beatmap to create the hit renderer for.</param>
/// <param name="isForCurrentRuleset">Whether to assume the beatmap is for the current ruleset.</param>
protected HitRenderer(WorkingBeatmap beatmap, bool isForCurrentRuleset)
: base(beatmap, isForCurrentRuleset)
{ {
InputManager.Add(content = new Container InputManager.Add(content = new Container
{ {

View File

@ -57,12 +57,17 @@ namespace osu.Game.Screens
beatmap.Value = localMap; beatmap.Value = localMap;
} }
beatmap.ValueChanged += OnBeatmapChanged;
if (osuGame != null) if (osuGame != null)
ruleset.BindTo(osuGame.Ruleset); ruleset.BindTo(osuGame.Ruleset);
} }
protected override void LoadComplete()
{
base.LoadComplete();
beatmap.ValueChanged += OnBeatmapChanged;
}
/// <summary> /// <summary>
/// The global Beatmap was changed. /// The global Beatmap was changed.
/// </summary> /// </summary>

View File

@ -41,12 +41,11 @@ namespace osu.Game.Screens.Play
get { return isCounting; } get { return isCounting; }
set set
{ {
if (value != isCounting) if (value == isCounting) return;
{
isCounting = value; isCounting = value;
foreach (var child in Children) foreach (var child in Children)
child.IsCounting = value; child.IsCounting = value;
}
} }
} }

View File

@ -8,11 +8,11 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Screens.Play.Pause;
using OpenTK; using OpenTK;
using OpenTK.Graphics; using OpenTK.Graphics;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
namespace osu.Game.Screens.Play namespace osu.Game.Screens.Play
@ -89,7 +89,7 @@ namespace osu.Game.Screens.Play
protected void AddButton(string text, Color4 colour, Action action) protected void AddButton(string text, Color4 colour, Action action)
{ {
Buttons.Add(new PauseButton Buttons.Add(new Button
{ {
Text = text, Text = text,
ButtonColour = colour, ButtonColour = colour,
@ -179,12 +179,6 @@ namespace osu.Game.Screens.Play
} }
} }
}, },
new PauseProgressBar
{
Origin = Anchor.BottomCentre,
Anchor = Anchor.BottomCentre,
Width = 1f
}
}; };
Retries = 0; Retries = 0;
@ -195,5 +189,15 @@ namespace osu.Game.Screens.Play
AlwaysReceiveInput = true; AlwaysReceiveInput = true;
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
} }
public class Button : DialogButton
{
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
SampleHover = audio.Sample.Get(@"Menu/menuclick");
SampleClick = audio.Sample.Get(@"Menu/menuback");
}
}
} }
} }

View File

@ -1,19 +0,0 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Screens.Play.Pause
{
public class PauseButton : DialogButton
{
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
SampleHover = audio.Sample.Get(@"Menu/menuclick");
SampleClick = audio.Sample.Get(@"Menu/menuback");
}
}
}

View File

@ -1,149 +0,0 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps;
using OpenTK.Graphics;
namespace osu.Game.Screens.Play.Pause
{
public class PauseProgressBar : Container
{
private readonly Color4 fillColour = new Color4(221, 255, 255, 255);
private readonly Color4 glowColour = new Color4(221, 255, 255, 150);
private readonly Container fill;
private WorkingBeatmap current;
[BackgroundDependencyLoader]
private void load(OsuGameBase osuGame)
{
current = osuGame.Beatmap.Value;
}
protected override void Update()
{
base.Update();
if (current?.TrackLoaded ?? false)
{
fill.Width = (float)(current.Track.CurrentTime / current.Track.Length);
}
}
public PauseProgressBar()
{
RelativeSizeAxes = Axes.X;
Height = 60;
Children = new Drawable[]
{
new PauseProgressGraph
{
RelativeSizeAxes = Axes.X,
Origin = Anchor.BottomCentre,
Anchor = Anchor.BottomCentre,
Height = 35,
Margin = new MarginPadding
{
Bottom = 5
}
},
new Container
{
Origin = Anchor.BottomRight,
Anchor = Anchor.BottomRight,
RelativeSizeAxes = Axes.X,
Height = 5,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
Alpha = 0.5f
}
}
},
fill = new Container
{
RelativeSizeAxes = Axes.X,
Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft,
Width = 0,
Height = 60,
Children = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft,
Masking = true,
Children = new Drawable[]
{
new Container
{
Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft,
RelativeSizeAxes = Axes.X,
Height = 5,
Masking = true,
EdgeEffect = new EdgeEffect
{
Type = EdgeEffectType.Glow,
Colour = glowColour,
Radius = 5
},
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = fillColour
}
}
}
}
},
new Container
{
Origin = Anchor.BottomRight,
Anchor = Anchor.BottomRight,
Width = 2,
Height = 35,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.White
},
new Container
{
Origin = Anchor.BottomCentre,
Anchor = Anchor.TopCentre,
Width = 14,
Height = 25,
CornerRadius = 5,
Masking = true,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.White
}
}
}
}
}
}
}
};
}
}
}

View File

@ -1,12 +0,0 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Graphics.Containers;
namespace osu.Game.Screens.Play.Pause
{
public class PauseProgressGraph : Container
{
// TODO: Implement the pause progress graph
}
}

View File

@ -0,0 +1,155 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Timing;
using osu.Game.Graphics;
using OpenTK.Graphics;
using OpenTK.Input;
namespace osu.Game.Screens.Play
{
/// <summary>
/// A container which handles pausing children, displaying a pause overlay with choices etc.
/// This alleviates a lot of the intricate pause logic from being in <see cref="Player"/>
/// </summary>
public class PauseContainer : Container
{
public bool IsPaused { get; private set; }
public bool AllowExit => IsPaused && pauseOverlay.Alpha == 1;
public Func<bool> CheckCanPause;
private const double pause_cooldown = 1000;
private double lastPauseActionTime;
private readonly PauseOverlay pauseOverlay;
private readonly Container content;
protected override Container<Drawable> Content => content;
public int Retries { set { pauseOverlay.Retries = value; } }
public bool CanPause => (CheckCanPause?.Invoke() ?? true) && Time.Current >= lastPauseActionTime + pause_cooldown;
public Action OnRetry;
public Action OnQuit;
public Action OnResume;
public Action OnPause;
public IAdjustableClock AudioClock;
public FramedClock FramedClock;
public PauseContainer()
{
RelativeSizeAxes = Axes.Both;
AddInternal(content = new Container { RelativeSizeAxes = Axes.Both });
AddInternal(pauseOverlay = new PauseOverlay
{
OnResume = delegate
{
Delay(400);
Schedule(Resume);
},
OnRetry = () => OnRetry(),
OnQuit = () => OnQuit(),
});
}
public void Pause(bool force = false)
{
if (!CanPause && !force) return;
if (IsPaused) return;
// stop the decoupled clock (stops the audio eventually)
AudioClock.Stop();
// stop processing updatess on the offset clock (instantly freezes time for all our components)
FramedClock.ProcessSourceClockFrames = false;
IsPaused = true;
// we need to do a final check after all of our children have processed up to the paused clock time.
// this is to cover cases where, for instance, the player fails in the current processing frame.
Schedule(() =>
{
if (!CanPause) return;
lastPauseActionTime = Time.Current;
OnPause?.Invoke();
pauseOverlay.Show();
});
}
public void Resume()
{
if (!IsPaused) return;
IsPaused = false;
FramedClock.ProcessSourceClockFrames = true;
lastPauseActionTime = Time.Current;
OnResume?.Invoke();
pauseOverlay.Hide();
AudioClock.Start();
}
private OsuGameBase game;
[BackgroundDependencyLoader]
private void load(OsuGameBase game)
{
this.game = game;
}
protected override void Update()
{
// eagerly pause when we lose window focus (if we are locally playing).
if (!game.IsActive && CanPause)
Pause();
base.Update();
}
public class PauseOverlay : MenuOverlay
{
public Action OnResume;
public override string Header => "paused";
public override string Description => "you're not going to do what i think you're going to do, are ya?";
protected override bool OnKeyDown(InputState state, KeyDownEventArgs args)
{
if (!args.Repeat && args.Key == Key.Escape)
{
Buttons.Children.First().TriggerClick();
return true;
}
return base.OnKeyDown(state, args);
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
AddButton("Continue", colours.Green, OnResume);
AddButton("Retry", colours.YellowDark, OnRetry);
AddButton("Quit", new Color4(170, 27, 39, 255), OnQuit);
}
}
}
}

View File

@ -1,40 +0,0 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Linq;
using osu.Framework.Input;
using osu.Game.Graphics;
using OpenTK.Input;
using OpenTK.Graphics;
using osu.Framework.Allocation;
namespace osu.Game.Screens.Play
{
public class PauseOverlay : MenuOverlay
{
public Action OnResume;
public override string Header => "paused";
public override string Description => "you're not going to do what i think you're going to do, are ya?";
protected override bool OnKeyDown(InputState state, KeyDownEventArgs args)
{
if (!args.Repeat && args.Key == Key.Escape)
{
Buttons.Children.First().TriggerClick();
return true;
}
return base.OnKeyDown(state, args);
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
AddButton("Continue", colours.Green, OnResume);
AddButton("Retry", colours.YellowDark, OnRetry);
AddButton("Quit", new Color4(170, 27, 39, 255), OnQuit);
}
}
}

View File

@ -32,29 +32,24 @@ namespace osu.Game.Screens.Play
internal override bool ShowOverlays => false; internal override bool ShowOverlays => false;
internal override bool HasLocalCursorDisplayed => !IsPaused && !HasFailed && HitRenderer.ProvidingUserCursor; internal override bool HasLocalCursorDisplayed => !pauseContainer.IsPaused && !HasFailed && HitRenderer.ProvidingUserCursor;
public BeatmapInfo BeatmapInfo; public BeatmapInfo BeatmapInfo;
public Action RestartRequested; public Action RestartRequested;
public bool IsPaused => !decoupledClock.IsRunning;
internal override bool AllowRulesetChange => false; internal override bool AllowRulesetChange => false;
public bool HasFailed { get; private set; } public bool HasFailed { get; private set; }
public int RestartCount; public int RestartCount;
private const double pause_cooldown = 1000;
private double lastPauseActionTime;
private bool canPause => ValidForResume && !HasFailed && Time.Current >= lastPauseActionTime + pause_cooldown;
private IAdjustableClock adjustableSourceClock; private IAdjustableClock adjustableSourceClock;
private FramedOffsetClock offsetClock; private FramedOffsetClock offsetClock;
private DecoupleableInterpolatingFramedClock decoupledClock; private DecoupleableInterpolatingFramedClock decoupledClock;
private PauseContainer pauseContainer;
private RulesetInfo ruleset; private RulesetInfo ruleset;
private ScoreProcessor scoreProcessor; private ScoreProcessor scoreProcessor;
@ -68,12 +63,7 @@ namespace osu.Game.Screens.Play
#endregion #endregion
private SkipButton skipButton;
private Container hitRendererContainer;
private HUDOverlay hudOverlay; private HUDOverlay hudOverlay;
private PauseOverlay pauseOverlay;
private FailOverlay failOverlay; private FailOverlay failOverlay;
[BackgroundDependencyLoader(permitNulls: true)] [BackgroundDependencyLoader(permitNulls: true)]
@ -97,7 +87,7 @@ namespace osu.Game.Screens.Play
try try
{ {
HitRenderer = rulesetInstance.CreateHitRendererWith(Beatmap); HitRenderer = rulesetInstance.CreateHitRendererWith(Beatmap, ruleset.ID == Beatmap.BeatmapInfo.Ruleset.ID);
} }
catch (BeatmapInvalidForRulesetException) catch (BeatmapInvalidForRulesetException)
{ {
@ -105,7 +95,7 @@ namespace osu.Game.Screens.Play
// let's try again forcing the beatmap's ruleset. // let's try again forcing the beatmap's ruleset.
ruleset = Beatmap.BeatmapInfo.Ruleset; ruleset = Beatmap.BeatmapInfo.Ruleset;
rulesetInstance = ruleset.CreateInstance(); rulesetInstance = ruleset.CreateInstance();
HitRenderer = rulesetInstance.CreateHitRendererWith(Beatmap); HitRenderer = rulesetInstance.CreateHitRendererWith(Beatmap, true);
} }
if (!HitRenderer.Objects.Any()) if (!HitRenderer.Objects.Any())
@ -152,14 +142,59 @@ namespace osu.Game.Screens.Play
decoupledClock.ChangeSource(adjustableSourceClock); decoupledClock.ChangeSource(adjustableSourceClock);
}); });
scoreProcessor = HitRenderer.CreateScoreProcessor(); Children = new Drawable[]
hudOverlay = new StandardHUDOverlay()
{ {
Anchor = Anchor.Centre, pauseContainer = new PauseContainer
Origin = Anchor.Centre {
AudioClock = decoupledClock,
FramedClock = offsetClock,
OnRetry = Restart,
OnQuit = Exit,
CheckCanPause = () => ValidForResume && !HasFailed && !HitRenderer.HasReplayLoaded,
Retries = RestartCount,
OnPause = () => {
hudOverlay.KeyCounter.IsCounting = pauseContainer.IsPaused;
},
OnResume = () => {
hudOverlay.KeyCounter.IsCounting = true;
},
Children = new Drawable[]
{
new SkipButton(firstObjectTime) { AudioClock = decoupledClock },
new Container
{
RelativeSizeAxes = Axes.Both,
Clock = offsetClock,
Children = new Drawable[]
{
HitRenderer,
}
},
hudOverlay = new StandardHUDOverlay
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
},
}
},
failOverlay = new FailOverlay
{
OnRetry = Restart,
OnQuit = Exit,
},
new HotkeyRetryOverlay
{
Action = () => {
//we want to hide the hitrenderer immediately (looks better).
//we may be able to remove this once the mouse cursor trail is improved.
HitRenderer?.Hide();
Restart();
},
}
}; };
scoreProcessor = HitRenderer.CreateScoreProcessor();
hudOverlay.KeyCounter.Add(rulesetInstance.CreateGameplayKeys()); hudOverlay.KeyCounter.Add(rulesetInstance.CreateGameplayKeys());
hudOverlay.BindProcessor(scoreProcessor); hudOverlay.BindProcessor(scoreProcessor);
hudOverlay.BindHitRenderer(HitRenderer); hudOverlay.BindHitRenderer(HitRenderer);
@ -176,126 +211,6 @@ namespace osu.Game.Screens.Play
//bind ScoreProcessor to ourselves (for a fail situation) //bind ScoreProcessor to ourselves (for a fail situation)
scoreProcessor.Failed += onFail; scoreProcessor.Failed += onFail;
Children = new Drawable[]
{
hitRendererContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Clock = offsetClock,
Children = new Drawable[]
{
HitRenderer,
skipButton = new SkipButton { Alpha = 0 },
}
},
}
},
hudOverlay,
pauseOverlay = new PauseOverlay
{
OnResume = delegate
{
Delay(400);
Schedule(Resume);
},
OnRetry = Restart,
OnQuit = Exit,
},
failOverlay = new FailOverlay
{
OnRetry = Restart,
OnQuit = Exit,
},
new HotkeyRetryOverlay
{
Action = () => {
//we want to hide the hitrenderer immediately (looks better).
//we may be able to remove this once the mouse cursor trail is improved.
HitRenderer?.Hide();
Restart();
},
}
};
}
protected override void Update()
{
// eagerly pause when we lose window focus (if we are locally playing).
if (!Game.IsActive && !HitRenderer.HasReplayLoaded)
Pause();
base.Update();
}
private void initializeSkipButton()
{
const double skip_required_cutoff = 3000;
const double fade_time = 300;
double firstHitObject = Beatmap.Beatmap.HitObjects.First().StartTime;
if (firstHitObject < skip_required_cutoff)
{
skipButton.Alpha = 0;
skipButton.Expire();
return;
}
skipButton.FadeInFromZero(fade_time);
skipButton.Action = () =>
{
decoupledClock.Seek(firstHitObject - skip_required_cutoff - fade_time);
skipButton.Action = null;
};
skipButton.Delay(firstHitObject - skip_required_cutoff - fade_time);
skipButton.FadeOut(fade_time);
skipButton.Expire();
}
public void Pause(bool force = false)
{
if (!canPause && !force) return;
// the actual pausing is potentially happening on a different thread.
// we want to wait for the source clock to stop so we can be sure all components are in a stable state.
if (!IsPaused)
{
decoupledClock.Stop();
Schedule(() => Pause(force));
return;
}
// we need to do a final check after all of our children have processed up to the paused clock time.
// this is to cover cases where, for instance, the player fails in the last processed frame (which would change canPause).
// as the scheduler runs before children updates, let's schedule for the next frame.
Schedule(() =>
{
if (!canPause) return;
lastPauseActionTime = Time.Current;
hudOverlay.KeyCounter.IsCounting = false;
hudOverlay.Progress.Show();
pauseOverlay.Retries = RestartCount;
pauseOverlay.Show();
});
}
public void Resume()
{
lastPauseActionTime = Time.Current;
hudOverlay.KeyCounter.IsCounting = true;
hudOverlay.Progress.Hide();
pauseOverlay.Hide();
decoupledClock.Start();
} }
public void Restart() public void Restart()
@ -315,18 +230,20 @@ namespace osu.Game.Screens.Play
ValidForResume = false; ValidForResume = false;
Delay(1000); using (BeginDelayedSequence(1000))
onCompletionEvent = Schedule(delegate
{ {
var score = new Score onCompletionEvent = Schedule(delegate
{ {
Beatmap = Beatmap.BeatmapInfo, var score = new Score
Ruleset = ruleset {
}; Beatmap = Beatmap.BeatmapInfo,
scoreProcessor.PopulateScore(score); Ruleset = ruleset
score.User = HitRenderer.Replay?.User ?? (Game as OsuGame)?.API?.LocalUser?.Value; };
Push(new Results(score)); scoreProcessor.PopulateScore(score);
}); score.User = HitRenderer.Replay?.User ?? (Game as OsuGame)?.API?.LocalUser?.Value;
Push(new Results(score));
});
}
} }
private void onFail() private void onFail()
@ -351,20 +268,21 @@ namespace osu.Game.Screens.Play
Content.ScaleTo(0.7f); Content.ScaleTo(0.7f);
Content.Delay(250); using (Content.BeginDelayedSequence(250))
Content.FadeIn(250); Content.FadeIn(250);
Content.ScaleTo(1, 750, EasingTypes.OutQuint); Content.ScaleTo(1, 750, EasingTypes.OutQuint);
Delay(750); using (BeginDelayedSequence(750))
Schedule(() => Schedule(() =>
{ {
decoupledClock.Start(); if (!pauseContainer.IsPaused)
initializeSkipButton(); decoupledClock.Start();
});
hitRendererContainer.Alpha = 0; });
hitRendererContainer.FadeIn(750, EasingTypes.OutQuint);
pauseContainer.Alpha = 0;
pauseContainer.FadeIn(750, EasingTypes.OutQuint);
} }
protected override void OnSuspending(Screen next) protected override void OnSuspending(Screen next)
@ -375,23 +293,14 @@ namespace osu.Game.Screens.Play
protected override bool OnExiting(Screen next) protected override bool OnExiting(Screen next)
{ {
if (!HasFailed && ValidForResume) if (HasFailed || !ValidForResume || pauseContainer.AllowExit || HitRenderer.HasReplayLoaded)
{ {
if (pauseOverlay != null && !HitRenderer.HasReplayLoaded) fadeOut();
{ return base.OnExiting(next);
//pause screen override logic.
if (pauseOverlay?.State == Visibility.Hidden && !canPause) return true;
if (!IsPaused) // For if the user presses escape quickly when entering the map
{
Pause();
return true;
}
}
} }
fadeOut(); pauseContainer.Pause();
return base.OnExiting(next); return true;
} }
private void fadeOut() private void fadeOut()
@ -406,6 +315,6 @@ namespace osu.Game.Screens.Play
Background?.FadeTo(1f, fade_out_duration); Background?.FadeTo(1f, fade_out_duration);
} }
protected override bool OnWheel(InputState state) => mouseWheelDisabled.Value && !IsPaused; protected override bool OnWheel(InputState state) => mouseWheelDisabled.Value && !pauseContainer.IsPaused;
} }
} }

View File

@ -1,32 +1,123 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>. // Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using osu.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Threading;
using osu.Framework.Timing;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.Sprites;
using osu.Game.Screens.Ranking;
using OpenTK;
using OpenTK.Graphics;
using OpenTK.Input; using OpenTK.Input;
using osu.Framework.Audio.Sample;
namespace osu.Game.Screens.Play namespace osu.Game.Screens.Play
{ {
public class SkipButton : TwoLayerButton public class SkipButton : Container
{ {
public SkipButton() private readonly double startTime;
public IAdjustableClock AudioClock;
private Button button;
private Box remainingTimeBox;
private FadeContainer fadeContainer;
private double displayTime;
public SkipButton(double startTime)
{ {
Text = @"Skip"; AlwaysReceiveInput = true;
Icon = FontAwesome.fa_osu_right_o;
Anchor = Anchor.BottomRight; this.startTime = startTime;
Origin = Anchor.BottomRight;
RelativePositionAxes = Axes.Both;
RelativeSizeAxes = Axes.Both;
Position = new Vector2(0.5f, 0.7f);
Size = new Vector2(1, 0.14f);
Origin = Anchor.Centre;
}
protected override bool OnMouseMove(InputState state)
{
fadeContainer.State = Visibility.Visible;
return base.OnMouseMove(state);
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(AudioManager audio, OsuColour colours) private void load(OsuColour colours)
{ {
ActivationSound = audio.Sample.Get(@"Menu/menuhit"); var baseClock = Clock;
BackgroundColour = colours.Yellow;
HoverColour = colours.YellowDark; if (AudioClock != null)
Clock = new FramedClock(AudioClock) { ProcessSourceClockFrames = false };
Children = new Drawable[]
{
fadeContainer = new FadeContainer
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
button = new Button
{
Clock = baseClock,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
remainingTimeBox = new Box
{
Height = 5,
RelativeSizeAxes = Axes.X,
Colour = colours.Yellow,
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
}
}
}
};
}
private const double skip_required_cutoff = 3000;
private const double fade_time = 300;
private double beginFadeTime => startTime - skip_required_cutoff - fade_time;
protected override void LoadComplete()
{
base.LoadComplete();
if (startTime < skip_required_cutoff)
{
Alpha = 0;
Expire();
return;
}
FadeInFromZero(fade_time);
using (BeginAbsoluteSequence(beginFadeTime))
FadeOut(fade_time);
button.Action = () => AudioClock?.Seek(startTime - skip_required_cutoff - fade_time);
displayTime = Time.Current;
Expire();
}
protected override void Update()
{
base.Update();
remainingTimeBox.ResizeWidthTo((float)Math.Max(0, 1 - (Time.Current - displayTime) / (beginFadeTime - displayTime)), 120, EasingTypes.OutQuint);
} }
protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) protected override bool OnKeyDown(InputState state, KeyDownEventArgs args)
@ -36,11 +127,171 @@ namespace osu.Game.Screens.Play
switch (args.Key) switch (args.Key)
{ {
case Key.Space: case Key.Space:
TriggerClick(); button.TriggerClick();
return true; return true;
} }
return base.OnKeyDown(state, args); return base.OnKeyDown(state, args);
} }
private class FadeContainer : Container, IStateful<Visibility>
{
private Visibility state;
private ScheduledDelegate scheduledHide;
public Visibility State
{
get
{
return state;
}
set
{
var lastState = state;
state = value;
scheduledHide?.Cancel();
switch (state)
{
case Visibility.Visible:
if (lastState == Visibility.Hidden)
FadeIn(500, EasingTypes.OutExpo);
if (!Hovering)
using (BeginDelayedSequence(1000))
scheduledHide = Schedule(() => State = Visibility.Hidden);
break;
case Visibility.Hidden:
FadeOut(1000, EasingTypes.OutExpo);
break;
}
}
}
protected override void LoadComplete()
{
base.LoadComplete();
State = Visibility.Visible;
}
}
private class Button : Container
{
public Action Action;
private Color4 colourNormal;
private Color4 colourHover;
private Box box;
private FillFlowContainer flow;
private Box background;
private AspectContainer aspect;
private SampleChannel activationSound;
public Button()
{
RelativeSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours, AudioManager audio)
{
activationSound = audio.Sample.Get(@"Menu/menuhit");
colourNormal = colours.Yellow;
colourHover = colours.YellowDark;
Children = new Drawable[]
{
background = new Box
{
Alpha = 0.2f,
Colour = Color4.Black,
RelativeSizeAxes = Axes.Both,
},
aspect = new AspectContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Y,
Height = 0.6f,
Masking = true,
CornerRadius = 15,
Children = new Drawable[]
{
box = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourNormal,
},
flow = new FillFlowContainer
{
Anchor = Anchor.TopCentre,
RelativePositionAxes = Axes.Y,
Y = 0.4f,
AutoSizeAxes = Axes.Both,
Origin = Anchor.Centre,
Direction = FillDirection.Horizontal,
Children = new []
{
new TextAwesome { Icon = FontAwesome.fa_chevron_right },
new TextAwesome { Icon = FontAwesome.fa_chevron_right },
new TextAwesome { Icon = FontAwesome.fa_chevron_right },
}
},
new OsuSpriteText
{
Anchor = Anchor.TopCentre,
RelativePositionAxes = Axes.Y,
Y = 0.7f,
TextSize = 12,
Font = @"Exo2.0-Bold",
Origin = Anchor.Centre,
Text = @"SKIP",
},
}
}
};
}
protected override bool OnHover(InputState state)
{
flow.TransformSpacingTo(new Vector2(5), 500, EasingTypes.OutQuint);
box.FadeColour(colourHover, 500, EasingTypes.OutQuint);
background.FadeTo(0.4f, 500, EasingTypes.OutQuint);
return base.OnHover(state);
}
protected override void OnHoverLost(InputState state)
{
flow.TransformSpacingTo(new Vector2(0), 500, EasingTypes.OutQuint);
box.FadeColour(colourNormal, 500, EasingTypes.OutQuint);
background.FadeTo(0.2f, 500, EasingTypes.OutQuint);
base.OnHoverLost(state);
}
protected override bool OnMouseDown(InputState state, MouseDownEventArgs args)
{
aspect.ScaleTo(0.75f, 2000, EasingTypes.OutQuint);
return base.OnMouseDown(state, args);
}
protected override bool OnMouseUp(InputState state, MouseUpEventArgs args)
{
aspect.ScaleTo(1, 1000, EasingTypes.OutElastic);
return base.OnMouseUp(state, args);
}
protected override bool OnClick(InputState state)
{
Action?.Invoke();
activationSound.Play();
box.FlashColour(Color4.White, 500, EasingTypes.OutQuint);
aspect.ScaleTo(1.2f, 2000, EasingTypes.OutQuint);
return true;
}
}
} }
} }

View File

@ -31,6 +31,8 @@ namespace osu.Game.Screens.Play
public Action<double> OnSeek; public Action<double> OnSeek;
public override bool HandleInput => AllowSeeking;
private IClock audioClock; private IClock audioClock;
public IClock AudioClock { set { audioClock = info.AudioClock = value; } } public IClock AudioClock { set { audioClock = info.AudioClock = value; } }

View File

@ -107,6 +107,8 @@ namespace osu.Game.Screens.Select
return; return;
} }
if (beatmap == SelectedBeatmap) return;
foreach (BeatmapGroup group in groups) foreach (BeatmapGroup group in groups)
{ {
var panel = group.BeatmapPanels.FirstOrDefault(p => p.Beatmap.Equals(beatmap)); var panel = group.BeatmapPanels.FirstOrDefault(p => p.Beatmap.Equals(beatmap));
@ -153,7 +155,11 @@ namespace osu.Game.Screens.Select
index = (index + direction + groups.Count) % groups.Count; index = (index + direction + groups.Count) % groups.Count;
if (groups[index].State != BeatmapGroupState.Hidden) if (groups[index].State != BeatmapGroupState.Hidden)
{ {
SelectBeatmap(groups[index].BeatmapPanels.First().Beatmap); if (skipDifficulties)
SelectBeatmap(groups[index].SelectedPanel != null ? groups[index].SelectedPanel.Beatmap : groups[index].BeatmapPanels.First().Beatmap);
else
SelectBeatmap(direction == 1 ? groups[index].BeatmapPanels.First().Beatmap : groups[index].BeatmapPanels.Last().Beatmap);
return; return;
} }
} while (index != startIndex); } while (index != startIndex);
@ -165,10 +171,8 @@ namespace osu.Game.Screens.Select
if (visibleGroups.Count < 1) if (visibleGroups.Count < 1)
return; return;
BeatmapGroup group = visibleGroups[RNG.Next(visibleGroups.Count)]; BeatmapGroup group = visibleGroups[RNG.Next(visibleGroups.Count)];
BeatmapPanel panel = group?.BeatmapPanels.First();
if (panel == null) BeatmapPanel panel = group.BeatmapPanels[RNG.Next(group.BeatmapPanels.Count)];
return;
selectGroup(group, panel); selectGroup(group, panel);
} }
@ -204,7 +208,7 @@ namespace osu.Game.Screens.Select
if (selectedGroup == null || selectedGroup.State == BeatmapGroupState.Hidden) if (selectedGroup == null || selectedGroup.State == BeatmapGroupState.Hidden)
SelectNext(); SelectNext();
else else
selectGroup(selectedGroup); selectGroup(selectedGroup, selectedPanel);
}; };
filterTask?.Cancel(); filterTask?.Cancel();
@ -339,6 +343,8 @@ namespace osu.Game.Screens.Select
selectedGroup.State = BeatmapGroupState.Collapsed; selectedGroup.State = BeatmapGroupState.Collapsed;
group.State = BeatmapGroupState.Expanded; group.State = BeatmapGroupState.Expanded;
group.SelectedPanel = panel;
panel.State = PanelSelectedState.Selected; panel.State = PanelSelectedState.Selected;
if (selectedPanel == panel) return; if (selectedPanel == panel) return;
@ -405,7 +411,14 @@ namespace osu.Game.Screens.Select
int firstIndex = yPositions.BinarySearch(Current - Panel.MAX_HEIGHT); int firstIndex = yPositions.BinarySearch(Current - Panel.MAX_HEIGHT);
if (firstIndex < 0) firstIndex = ~firstIndex; if (firstIndex < 0) firstIndex = ~firstIndex;
int lastIndex = yPositions.BinarySearch(Current + drawHeight); int lastIndex = yPositions.BinarySearch(Current + drawHeight);
if (lastIndex < 0) lastIndex = ~lastIndex; if (lastIndex < 0)
{
lastIndex = ~lastIndex;
// Add the first panel of the last visible beatmap group to preload its data.
if (lastIndex != 0 && panels[lastIndex - 1] is BeatmapSetHeader)
lastIndex++;
}
// Add those panels within the previously found index range that should be displayed. // Add those panels within the previously found index range that should be displayed.
for (int i = firstIndex; i < lastIndex; ++i) for (int i = firstIndex; i < lastIndex; ++i)

View File

@ -49,6 +49,8 @@ namespace osu.Game.Screens.Select
get { return beatmap; } get { return beatmap; }
set set
{ {
if (beatmap == value) return;
beatmap = value; beatmap = value;
pendingBeatmapSwitch?.Cancel(); pendingBeatmapSwitch?.Cancel();

View File

@ -9,6 +9,7 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
@ -68,8 +69,6 @@ namespace osu.Game.Screens.Select
public Footer() public Footer()
{ {
AlwaysReceiveInput = true;
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
Height = HEIGHT; Height = HEIGHT;
Anchor = Anchor.BottomCentre; Anchor = Anchor.BottomCentre;
@ -124,5 +123,13 @@ namespace osu.Game.Screens.Select
updateModeLight(); updateModeLight();
} }
protected override bool InternalContains(Vector2 screenSpacePos) => base.InternalContains(screenSpacePos) || StartButton.Contains(screenSpacePos);
protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) => true;
protected override bool OnClick(InputState state) => true;
protected override bool OnDragStart(InputState state) => true;
} }
} }

View File

@ -100,6 +100,8 @@ namespace osu.Game.Screens.Select.Leaderboards
get { return beatmap; } get { return beatmap; }
set set
{ {
if (beatmap == value) return;
beatmap = value; beatmap = value;
Scores = null; Scores = null;

View File

@ -57,14 +57,23 @@ namespace osu.Game.Screens.Select
{ {
beatmap?.Mods.BindTo(modSelect.SelectedMods); beatmap?.Mods.BindTo(modSelect.SelectedMods);
if (Beatmap?.Track != null)
Beatmap.Track.Looping = false;
beatmapDetails.Beatmap = beatmap; beatmapDetails.Beatmap = beatmap;
if (beatmap?.Track != null)
beatmap.Track.Looping = true;
base.OnBeatmapChanged(beatmap); base.OnBeatmapChanged(beatmap);
} }
protected override void OnResuming(Screen last) protected override void OnResuming(Screen last)
{ {
player = null; player = null;
Beatmap.Track.Looping = true;
base.OnResuming(last); base.OnResuming(last);
} }
@ -83,13 +92,21 @@ namespace osu.Game.Screens.Select
return true; return true;
} }
return base.OnExiting(next); if (base.OnExiting(next))
return true;
if (Beatmap?.Track != null)
Beatmap.Track.Looping = false;
return false;
} }
protected override void OnSelected() protected override void OnSelected()
{ {
if (player != null) return; if (player != null) return;
Beatmap.Track.Looping = false;
LoadComponentAsync(player = new PlayerLoader(new Player LoadComponentAsync(player = new PlayerLoader(new Player
{ {
Beatmap = Beatmap, //eagerly set this so it's present before push. Beatmap = Beatmap, //eagerly set this so it's present before push.

View File

@ -198,8 +198,11 @@ namespace osu.Game.Screens.Select
var pendingSelection = selectionChangedDebounce; var pendingSelection = selectionChangedDebounce;
selectionChangedDebounce = null; selectionChangedDebounce = null;
pendingSelection?.RunTask(); if (pendingSelection?.Completed == false)
pendingSelection?.Cancel(); // cancel the already scheduled task. {
pendingSelection?.RunTask();
pendingSelection?.Cancel(); // cancel the already scheduled task.
}
if (Beatmap == null) return; if (Beatmap == null) return;
@ -226,6 +229,8 @@ namespace osu.Game.Screens.Select
changeBackground(Beatmap); changeBackground(Beatmap);
selectionChangeNoBounce = Beatmap?.BeatmapInfo;
Content.FadeInFromZero(250); Content.FadeInFromZero(250);
beatmapInfoWedge.State = Visibility.Visible; beatmapInfoWedge.State = Visibility.Visible;
@ -313,15 +318,15 @@ namespace osu.Game.Screens.Select
{ {
bool beatmapSetChange = false; bool beatmapSetChange = false;
if (!beatmap.Equals(Beatmap?.BeatmapInfo)) if (beatmap.Equals(Beatmap?.BeatmapInfo))
return;
if (beatmap.BeatmapSetInfoID == selectionChangeNoBounce?.BeatmapSetInfoID)
sampleChangeDifficulty.Play();
else
{ {
if (beatmap.BeatmapSetInfoID == selectionChangeNoBounce?.BeatmapSetInfoID) sampleChangeBeatmap.Play();
sampleChangeDifficulty.Play(); beatmapSetChange = true;
else
{
sampleChangeBeatmap.Play();
beatmapSetChange = true;
}
} }
selectionChangeNoBounce = beatmap; selectionChangeNoBounce = beatmap;

View File

@ -81,12 +81,14 @@
<Compile Include="Overlays\Music\PlaylistItem.cs" /> <Compile Include="Overlays\Music\PlaylistItem.cs" />
<Compile Include="Overlays\Music\PlaylistList.cs" /> <Compile Include="Overlays\Music\PlaylistList.cs" />
<Compile Include="Overlays\OnScreenDisplay.cs" /> <Compile Include="Overlays\OnScreenDisplay.cs" />
<Compile Include="Graphics\Containers\SectionsContainer.cs" />
<Compile Include="Overlays\Settings\SettingsHeader.cs" /> <Compile Include="Overlays\Settings\SettingsHeader.cs" />
<Compile Include="Overlays\Settings\Sections\Audio\MainMenuSettings.cs" /> <Compile Include="Overlays\Settings\Sections\Audio\MainMenuSettings.cs" />
<Compile Include="Overlays\Toolbar\ToolbarChatButton.cs" /> <Compile Include="Overlays\Toolbar\ToolbarChatButton.cs" />
<Compile Include="Rulesets\Beatmaps\BeatmapConverter.cs" /> <Compile Include="Rulesets\Beatmaps\BeatmapConverter.cs" />
<Compile Include="Rulesets\Beatmaps\BeatmapProcessor.cs" /> <Compile Include="Rulesets\Beatmaps\BeatmapProcessor.cs" />
<Compile Include="Beatmaps\Legacy\LegacyBeatmap.cs" /> <Compile Include="Beatmaps\Legacy\LegacyBeatmap.cs" />
<Compile Include="Beatmaps\Timing\BreakPeriod.cs" />
<Compile Include="Beatmaps\Timing\TimeSignatures.cs" /> <Compile Include="Beatmaps\Timing\TimeSignatures.cs" />
<Compile Include="Beatmaps\Timing\TimingInfo.cs" /> <Compile Include="Beatmaps\Timing\TimingInfo.cs" />
<Compile Include="Database\BeatmapMetrics.cs" /> <Compile Include="Database\BeatmapMetrics.cs" />
@ -125,6 +127,7 @@
<Compile Include="Rulesets\Objects\Legacy\ConvertSlider.cs" /> <Compile Include="Rulesets\Objects\Legacy\ConvertSlider.cs" />
<Compile Include="Rulesets\Objects\Legacy\Mania\ConvertHit.cs" /> <Compile Include="Rulesets\Objects\Legacy\Mania\ConvertHit.cs" />
<Compile Include="Rulesets\Objects\Legacy\Mania\ConvertHitObjectParser.cs" /> <Compile Include="Rulesets\Objects\Legacy\Mania\ConvertHitObjectParser.cs" />
<Compile Include="Rulesets\Objects\Legacy\Mania\ConvertHold.cs" />
<Compile Include="Rulesets\Objects\Legacy\Mania\ConvertSlider.cs" /> <Compile Include="Rulesets\Objects\Legacy\Mania\ConvertSlider.cs" />
<Compile Include="Rulesets\Objects\Legacy\Mania\ConvertSpinner.cs" /> <Compile Include="Rulesets\Objects\Legacy\Mania\ConvertSpinner.cs" />
<Compile Include="Rulesets\Objects\Legacy\Osu\ConvertHitObjectParser.cs" /> <Compile Include="Rulesets\Objects\Legacy\Osu\ConvertHitObjectParser.cs" />
@ -161,7 +164,6 @@
<Compile Include="Rulesets\Objects\CircularArcApproximator.cs" /> <Compile Include="Rulesets\Objects\CircularArcApproximator.cs" />
<Compile Include="Rulesets\Objects\Legacy\Osu\ConvertHit.cs" /> <Compile Include="Rulesets\Objects\Legacy\Osu\ConvertHit.cs" />
<Compile Include="Rulesets\Objects\Legacy\ConvertHitObjectParser.cs" /> <Compile Include="Rulesets\Objects\Legacy\ConvertHitObjectParser.cs" />
<Compile Include="Rulesets\Objects\Legacy\ConvertHold.cs" />
<Compile Include="Rulesets\Objects\Legacy\Osu\ConvertSlider.cs" /> <Compile Include="Rulesets\Objects\Legacy\Osu\ConvertSlider.cs" />
<Compile Include="Rulesets\Objects\Legacy\Osu\ConvertSpinner.cs" /> <Compile Include="Rulesets\Objects\Legacy\Osu\ConvertSpinner.cs" />
<Compile Include="Rulesets\Objects\SliderCurve.cs" /> <Compile Include="Rulesets\Objects\SliderCurve.cs" />
@ -231,6 +233,7 @@
<Compile Include="Screens\Charts\ChartInfo.cs" /> <Compile Include="Screens\Charts\ChartInfo.cs" />
<Compile Include="Screens\Edit\Editor.cs" /> <Compile Include="Screens\Edit\Editor.cs" />
<Compile Include="Screens\Play\HotkeyRetryOverlay.cs" /> <Compile Include="Screens\Play\HotkeyRetryOverlay.cs" />
<Compile Include="Screens\Play\PauseContainer.cs" />
<Compile Include="Screens\Play\SongProgressInfo.cs" /> <Compile Include="Screens\Play\SongProgressInfo.cs" />
<Compile Include="Screens\Play\HUD\ModDisplay.cs" /> <Compile Include="Screens\Play\HUD\ModDisplay.cs" />
<Compile Include="Screens\Play\SquareGraph.cs" /> <Compile Include="Screens\Play\SquareGraph.cs" />
@ -254,8 +257,6 @@
<Compile Include="Screens\Play\FailOverlay.cs" /> <Compile Include="Screens\Play\FailOverlay.cs" />
<Compile Include="Screens\Play\MenuOverlay.cs" /> <Compile Include="Screens\Play\MenuOverlay.cs" />
<Compile Include="Screens\Play\KeyConversionInputManager.cs" /> <Compile Include="Screens\Play\KeyConversionInputManager.cs" />
<Compile Include="Screens\Play\PauseOverlay.cs" />
<Compile Include="Screens\Play\Pause\PauseButton.cs" />
<Compile Include="Screens\Play\PlayerInputManager.cs" /> <Compile Include="Screens\Play\PlayerInputManager.cs" />
<Compile Include="Screens\Play\PlayerLoader.cs" /> <Compile Include="Screens\Play\PlayerLoader.cs" />
<Compile Include="Screens\Play\ReplayPlayer.cs" /> <Compile Include="Screens\Play\ReplayPlayer.cs" />
@ -342,7 +343,6 @@
<Compile Include="Beatmaps\Formats\BeatmapDecoder.cs" /> <Compile Include="Beatmaps\Formats\BeatmapDecoder.cs" />
<Compile Include="Beatmaps\Formats\OsuLegacyDecoder.cs" /> <Compile Include="Beatmaps\Formats\OsuLegacyDecoder.cs" />
<Compile Include="Beatmaps\IO\OszArchiveReader.cs" /> <Compile Include="Beatmaps\IO\OszArchiveReader.cs" />
<Compile Include="Beatmaps\Events\EventType.cs" />
<Compile Include="Graphics\UserInterface\Volume\VolumeMeter.cs" /> <Compile Include="Graphics\UserInterface\Volume\VolumeMeter.cs" />
<Compile Include="Database\BeatmapSetInfo.cs" /> <Compile Include="Database\BeatmapSetInfo.cs" />
<Compile Include="Database\BeatmapMetadata.cs" /> <Compile Include="Database\BeatmapMetadata.cs" />
@ -395,8 +395,6 @@
<Compile Include="Screens\Play\SongProgress.cs" /> <Compile Include="Screens\Play\SongProgress.cs" />
<Compile Include="Screens\Play\SongProgressGraph.cs" /> <Compile Include="Screens\Play\SongProgressGraph.cs" />
<Compile Include="Screens\Play\SongProgressBar.cs" /> <Compile Include="Screens\Play\SongProgressBar.cs" />
<Compile Include="Screens\Play\Pause\PauseProgressBar.cs" />
<Compile Include="Screens\Play\Pause\PauseProgressGraph.cs" />
<Compile Include="Overlays\Mods\ModSelectOverlay.cs" /> <Compile Include="Overlays\Mods\ModSelectOverlay.cs" />
<Compile Include="Rulesets\Mods\Mod.cs" /> <Compile Include="Rulesets\Mods\Mod.cs" />
<Compile Include="Overlays\Mods\ModButtonEmpty.cs" /> <Compile Include="Overlays\Mods\ModButtonEmpty.cs" />
@ -430,11 +428,11 @@
<Compile Include="Rulesets\Replays\AutoGenerator.cs" /> <Compile Include="Rulesets\Replays\AutoGenerator.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="$(SolutionDir)\osu-framework\osu.Framework\osu.Framework.csproj"> <ProjectReference Include="..\osu-framework\osu.Framework\osu.Framework.csproj">
<Project>{c76bf5b3-985e-4d39-95fe-97c9c879b83a}</Project> <Project>{c76bf5b3-985e-4d39-95fe-97c9c879b83a}</Project>
<Name>osu.Framework</Name> <Name>osu.Framework</Name>
</ProjectReference> </ProjectReference>
<ProjectReference Include="$(SolutionDir)\osu-resources\osu.Game.Resources\osu.Game.Resources.csproj"> <ProjectReference Include="..\osu-resources\osu.Game.Resources\osu.Game.Resources.csproj">
<Project>{d9a367c9-4c1a-489f-9b05-a0cea2b53b58}</Project> <Project>{d9a367c9-4c1a-489f-9b05-a0cea2b53b58}</Project>
<Name>osu.Game.Resources</Name> <Name>osu.Game.Resources</Name>
</ProjectReference> </ProjectReference>