1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-15 00:53:22 +08:00

Merge branch 'master' into player_loader_input_toggles

This commit is contained in:
Dean Herbert 2018-05-21 16:20:08 +09:00 committed by GitHub
commit c48c62ea04
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
175 changed files with 3031 additions and 1574 deletions

@ -1 +1 @@
Subproject commit 0773d895d9aa0729995cd4a23efc28238e35ceed
Subproject commit fac688633b8fcf34ae5d0514c26b03e217161eb4

View File

@ -5,8 +5,6 @@ using System;
using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.MathUtils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects;
@ -14,7 +12,7 @@ using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Catch.Tests
{
public class CatchBeatmapConversionTest : BeatmapConversionTest<ConvertValue>
internal class CatchBeatmapConversionTest : BeatmapConversionTest<ConvertValue>
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Catch";
@ -47,10 +45,10 @@ namespace osu.Game.Rulesets.Catch.Tests
}
}
protected override IBeatmapConverter CreateConverter(Beatmap beatmap) => new CatchBeatmapConverter();
protected override Ruleset CreateRuleset() => new CatchRuleset();
}
public struct ConvertValue : IEquatable<ConvertValue>
internal struct ConvertValue : IEquatable<ConvertValue>
{
/// <summary>
/// A sane value to account for osu!stable using ints everwhere.

View File

@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Catch.Tests
{
}
protected override Beatmap CreateBeatmap(Ruleset ruleset)
protected override IBeatmap CreateBeatmap(Ruleset ruleset)
{
var beatmap = new Beatmap
{

View File

@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Catch.Tests
{
}
protected override Beatmap CreateBeatmap(Ruleset ruleset)
protected override IBeatmap CreateBeatmap(Ruleset ruleset)
{
var beatmap = new Beatmap
{

View File

@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Catch.Tests
{
}
protected override Beatmap CreateBeatmap(Ruleset ruleset)
protected override IBeatmap CreateBeatmap(Ruleset ruleset)
{
var beatmap = new Beatmap
{

View File

@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Catch.Tests
{
}
protected override Beatmap CreateBeatmap(Ruleset ruleset)
protected override IBeatmap CreateBeatmap(Ruleset ruleset)
{
var beatmap = new Beatmap { BeatmapInfo = { Ruleset = ruleset.RulesetInfo } };

View File

@ -0,0 +1,43 @@
// Copyright (c) 2007-2018 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.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Rulesets.Catch.Objects;
namespace osu.Game.Rulesets.Catch.Beatmaps
{
public class CatchBeatmap : Beatmap<CatchHitObject>
{
public override IEnumerable<BeatmapStatistic> GetStatistics()
{
int fruits = HitObjects.Count(s => s is Fruit);
int juiceStreams = HitObjects.Count(s => s is JuiceStream);
int bananaShowers = HitObjects.Count(s => s is BananaShower);
return new[]
{
new BeatmapStatistic
{
Name = @"Fruit Count",
Content = fruits.ToString(),
Icon = FontAwesome.fa_circle_o
},
new BeatmapStatistic
{
Name = @"Juice Stream Count",
Content = juiceStreams.ToString(),
Icon = FontAwesome.fa_circle
},
new BeatmapStatistic
{
Name = @"Banana Shower Count",
Content = bananaShowers.ToString(),
Icon = FontAwesome.fa_circle
}
};
}
}
}

View File

@ -13,9 +13,14 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
{
public class CatchBeatmapConverter : BeatmapConverter<CatchHitObject>
{
public CatchBeatmapConverter(IBeatmap beatmap)
: base(beatmap)
{
}
protected override IEnumerable<Type> ValidConversionTypes { get; } = new[] { typeof(IHasXPosition) };
protected override IEnumerable<CatchHitObject> ConvertHitObject(HitObject obj, Beatmap beatmap)
protected override IEnumerable<CatchHitObject> ConvertHitObject(HitObject obj, IBeatmap beatmap)
{
var curveData = obj as IHasCurve;
var positionData = obj as IHasXPosition;
@ -64,5 +69,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
X = positionData.X / CatchPlayfield.BASE_WIDTH
};
}
protected override Beatmap<CatchHitObject> CreateBeatmap() => new CatchBeatmap();
}
}

View File

@ -12,16 +12,21 @@ using OpenTK;
namespace osu.Game.Rulesets.Catch.Beatmaps
{
public class CatchBeatmapProcessor : BeatmapProcessor<CatchHitObject>
public class CatchBeatmapProcessor : BeatmapProcessor
{
public override void PostProcess(Beatmap<CatchHitObject> beatmap)
public CatchBeatmapProcessor(IBeatmap beatmap)
: base(beatmap)
{
initialiseHyperDash(beatmap.HitObjects);
}
base.PostProcess(beatmap);
public override void PostProcess()
{
initialiseHyperDash((List<CatchHitObject>)Beatmap.HitObjects);
base.PostProcess();
int index = 0;
foreach (var obj in beatmap.HitObjects)
foreach (var obj in Beatmap.HitObjects.OfType<CatchHitObject>())
obj.IndexInBeatmap = index++;
}

View File

@ -13,12 +13,17 @@ using osu.Framework.Input.Bindings;
using osu.Game.Rulesets.Catch.Replays;
using osu.Game.Rulesets.Replays.Types;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Catch.Difficulty;
using osu.Game.Rulesets.Difficulty;
namespace osu.Game.Rulesets.Catch
{
public class CatchRuleset : Ruleset
{
public override RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap, bool isForCurrentRuleset) => new CatchRulesetContainer(this, beatmap, isForCurrentRuleset);
public override RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap) => new CatchRulesetContainer(this, beatmap);
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new CatchBeatmapConverter(beatmap);
public override IBeatmapProcessor CreateBeatmapProcessor(IBeatmap beatmap) => new CatchBeatmapProcessor(beatmap);
public override IEnumerable<KeyBinding> GetDefaultKeyBindings(int variant = 0) => new[]
{
@ -138,7 +143,7 @@ namespace osu.Game.Rulesets.Catch
public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_fruits_o };
public override DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap, Mod[] mods = null) => new CatchDifficultyCalculator(beatmap);
public override DifficultyCalculator CreateDifficultyCalculator(IBeatmap beatmap, Mod[] mods = null) => new CatchDifficultyCalculator(beatmap);
public override int? LegacyID => 2;

View File

@ -1,21 +1,18 @@
// Copyright (c) 2007-2018 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.Catch.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
namespace osu.Game.Rulesets.Catch
namespace osu.Game.Rulesets.Catch.Difficulty
{
public class CatchDifficultyCalculator : DifficultyCalculator<CatchHitObject>
public class CatchDifficultyCalculator : DifficultyCalculator
{
public CatchDifficultyCalculator(Beatmap beatmap) : base(beatmap)
public CatchDifficultyCalculator(IBeatmap beatmap) : base(beatmap)
{
}
public override double Calculate(Dictionary<string, double> categoryDifficulty = null) => 0;
protected override BeatmapConverter<CatchHitObject> CreateBeatmapConverter(Beatmap beatmap) => new CatchBeatmapConverter();
}
}

View File

@ -6,10 +6,11 @@ using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Mods;
using System;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModHardRock : ModHardRock, IApplicableToHitObject<CatchHitObject>
public class CatchModHardRock : ModHardRock, IApplicableToHitObject
{
public override double ScoreMultiplier => 1.12;
public override bool Ranked => true;
@ -17,9 +18,11 @@ namespace osu.Game.Rulesets.Catch.Mods
private float lastStartX;
private int lastStartTime;
public void ApplyToHitObject(CatchHitObject hitObject)
public void ApplyToHitObject(HitObject hitObject)
{
float position = hitObject.X;
var catchObject = (CatchHitObject)hitObject;
float position = catchObject.X;
int startTime = (int)hitObject.StartTime;
if (lastStartX == 0)
@ -60,7 +63,7 @@ namespace osu.Game.Rulesets.Catch.Mods
position += rand;
}
hitObject.X = position;
catchObject.X = position;
return;
}
@ -79,7 +82,7 @@ namespace osu.Game.Rulesets.Catch.Mods
}
}
hitObject.X = position;
catchObject.X = position;
lastStartX = position;
lastStartTime = startTime;

View File

@ -47,6 +47,8 @@ namespace osu.Game.Rulesets.Catch.Objects
Scale = 1.0f - 0.7f * (difficulty.CircleSize - 5) / 5;
}
protected override HitWindows CreateHitWindows() => null;
}
public enum FruitVisualRepresentation

View File

@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Catch.Replays
Dashing = dashing;
}
public void ConvertFrom(LegacyReplayFrame legacyFrame, Beatmap beatmap)
public void ConvertFrom(LegacyReplayFrame legacyFrame, IBeatmap beatmap)
{
Position = legacyFrame.Position.X / CatchPlayfield.BASE_WIDTH;
Dashing = legacyFrame.ButtonState == ReplayButtonState.Left1;

View File

@ -4,7 +4,6 @@
using osu.Framework.Input;
using osu.Game.Beatmaps;
using osu.Game.Input.Handlers;
using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawable;
using osu.Game.Rulesets.Catch.Replays;
@ -20,8 +19,8 @@ namespace osu.Game.Rulesets.Catch.UI
{
public class CatchRulesetContainer : ScrollingRulesetContainer<CatchPlayfield, CatchHitObject>
{
public CatchRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap, bool isForCurrentRuleset)
: base(ruleset, beatmap, isForCurrentRuleset)
public CatchRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap)
{
}
@ -29,10 +28,6 @@ namespace osu.Game.Rulesets.Catch.UI
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new CatchFramedReplayInputHandler(replay);
protected override BeatmapProcessor<CatchHitObject> CreateBeatmapProcessor() => new CatchBeatmapProcessor();
protected override BeatmapConverter<CatchHitObject> CreateBeatmapConverter() => new CatchBeatmapConverter();
protected override Playfield CreatePlayfield() => new CatchPlayfield(Beatmap.BeatmapInfo.BaseDifficulty, GetVisualRepresentation);
public override PassThroughInputManager CreateInputManager() => new CatchInputManager(Ruleset.RulesetInfo);

View File

@ -5,8 +5,6 @@ using System;
using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.MathUtils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
@ -14,17 +12,14 @@ using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Mania.Tests
{
public class ManiaBeatmapConversionTest : BeatmapConversionTest<ConvertValue>
internal class ManiaBeatmapConversionTest : BeatmapConversionTest<ConvertValue>
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania";
private bool isForCurrentRuleset;
[NonParallelizable]
[TestCase("basic", false)]
public void Test(string name, bool isForCurrentRuleset)
[TestCase("basic")]
public new void Test(string name)
{
this.isForCurrentRuleset = isForCurrentRuleset;
base.Test(name);
}
@ -38,10 +33,10 @@ namespace osu.Game.Rulesets.Mania.Tests
};
}
protected override IBeatmapConverter CreateConverter(Beatmap beatmap) => new ManiaBeatmapConverter(isForCurrentRuleset, beatmap);
protected override Ruleset CreateRuleset() => new ManiaRuleset();
}
public struct ConvertValue : IEquatable<ConvertValue>
internal struct ConvertValue : IEquatable<ConvertValue>
{
/// <summary>
/// A sane value to account for osu!stable using ints everwhere.

View File

@ -4,6 +4,8 @@
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Tests.Visual;
@ -17,6 +19,14 @@ namespace osu.Game.Rulesets.Mania.Tests
{
public TestCaseManiaHitObjects()
{
Note note1 = new Note();
Note note2 = new Note();
HoldNote holdNote = new HoldNote { StartTime = 1000 };
note1.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
note2.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
holdNote.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
Add(new FillFlowContainer
{
Anchor = Anchor.Centre,
@ -43,14 +53,14 @@ namespace osu.Game.Rulesets.Mania.Tests
RelativeChildSize = new Vector2(1, 10000),
Children = new[]
{
new DrawableNote(new Note(), ManiaAction.Key1)
new DrawableNote(note1, ManiaAction.Key1)
{
Y = 5000,
LifetimeStart = double.MinValue,
LifetimeEnd = double.MaxValue,
AccentColour = Color4.Red
},
new DrawableNote(new Note(), ManiaAction.Key1)
new DrawableNote(note2, ManiaAction.Key1)
{
Y = 6000,
LifetimeStart = double.MinValue,
@ -77,13 +87,13 @@ namespace osu.Game.Rulesets.Mania.Tests
RelativeChildSize = new Vector2(1, 10000),
Children = new[]
{
new DrawableHoldNote(new HoldNote { Duration = 1000 } , ManiaAction.Key1)
new DrawableHoldNote(holdNote, ManiaAction.Key1)
{
Y = 5000,
Height = 1000,
LifetimeStart = double.MinValue,
LifetimeEnd = double.MaxValue,
AccentColour = Color4.Red
AccentColour = Color4.Red,
}
}
}

View File

@ -8,6 +8,8 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Configuration;
@ -83,13 +85,16 @@ namespace osu.Game.Rulesets.Mania.Tests
int col = rng.Next(0, 4);
var note = new DrawableNote(new Note { Column = col }, ManiaAction.Key1)
var note = new Note { Column = col };
note.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
var drawableNote = new DrawableNote(note, ManiaAction.Key1)
{
AccentColour = playfield.Columns.ElementAt(col).AccentColour
};
playfield.OnJudgement(note, new ManiaJudgement { Result = HitResult.Perfect });
playfield.Columns[col].OnJudgement(note, new ManiaJudgement { Result = HitResult.Perfect });
playfield.OnJudgement(drawableNote, new ManiaJudgement { Result = HitResult.Perfect });
playfield.Columns[col].OnJudgement(drawableNote, new ManiaJudgement { Result = HitResult.Perfect });
});
}
@ -162,32 +167,24 @@ namespace osu.Game.Rulesets.Mania.Tests
for (double t = start_time; t <= start_time + duration; t += 100)
{
playfield.Add(new DrawableNote(new Note
{
StartTime = t,
Column = 0
}, ManiaAction.Key1));
var note1 = new Note { StartTime = t, Column = 0 };
var note2 = new Note { StartTime = t, Column = 3 };
playfield.Add(new DrawableNote(new Note
{
StartTime = t,
Column = 3
}, ManiaAction.Key4));
note1.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
note2.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
playfield.Add(new DrawableNote(note1, ManiaAction.Key1));
playfield.Add(new DrawableNote(note2, ManiaAction.Key4));
}
playfield.Add(new DrawableHoldNote(new HoldNote
{
StartTime = start_time,
Duration = duration,
Column = 1
}, ManiaAction.Key2));
var holdNote1 = new HoldNote { StartTime = start_time, Duration = duration, Column = 1 };
var holdNote2 = new HoldNote { StartTime = start_time, Duration = duration, Column = 2 };
playfield.Add(new DrawableHoldNote(new HoldNote
{
StartTime = start_time,
Duration = duration,
Column = 2
}, ManiaAction.Key3));
holdNote1.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
holdNote2.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
playfield.Add(new DrawableHoldNote(holdNote1, ManiaAction.Key2));
playfield.Add(new DrawableHoldNote(holdNote2, ManiaAction.Key3));
}
}
}

View File

@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.UI;
@ -29,5 +30,27 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
{
Stages.Add(defaultStage);
}
public override IEnumerable<BeatmapStatistic> GetStatistics()
{
int notes = HitObjects.Count(s => s is Note);
int holdnotes = HitObjects.Count(s => s is HoldNote);
return new[]
{
new BeatmapStatistic
{
Name = @"Note Count",
Content = notes.ToString(),
Icon = FontAwesome.fa_circle_o
},
new BeatmapStatistic
{
Name = @"Hold Note Count",
Content = holdnotes.ToString(),
Icon = FontAwesome.fa_circle
},
};
}
}
}

View File

@ -33,18 +33,19 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
private ManiaBeatmap beatmap;
public ManiaBeatmapConverter(bool isForCurrentRuleset, Beatmap original)
public ManiaBeatmapConverter(IBeatmap beatmap)
: base(beatmap)
{
IsForCurrentRuleset = isForCurrentRuleset;
IsForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(new ManiaRuleset().RulesetInfo);
var roundedCircleSize = Math.Round(original.BeatmapInfo.BaseDifficulty.CircleSize);
var roundedOverallDifficulty = Math.Round(original.BeatmapInfo.BaseDifficulty.OverallDifficulty);
var roundedCircleSize = Math.Round(beatmap.BeatmapInfo.BaseDifficulty.CircleSize);
var roundedOverallDifficulty = Math.Round(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
if (isForCurrentRuleset)
if (IsForCurrentRuleset)
TargetColumns = (int)Math.Max(1, roundedCircleSize);
else
{
float percentSliderOrSpinner = (float)original.HitObjects.Count(h => h is IHasEndTime) / original.HitObjects.Count;
float percentSliderOrSpinner = (float)beatmap.HitObjects.Count(h => h is IHasEndTime) / beatmap.HitObjects.Count();
if (percentSliderOrSpinner < 0.2)
TargetColumns = 7;
else if (percentSliderOrSpinner < 0.3 || roundedCircleSize >= 5)
@ -56,7 +57,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
}
}
protected override Beatmap<ManiaHitObject> ConvertBeatmap(Beatmap original)
protected override Beatmap<ManiaHitObject> ConvertBeatmap(IBeatmap original)
{
BeatmapDifficulty difficulty = original.BeatmapInfo.BaseDifficulty;
@ -68,7 +69,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
protected override Beatmap<ManiaHitObject> CreateBeatmap() => beatmap = new ManiaBeatmap(new StageDefinition { Columns = TargetColumns });
protected override IEnumerable<ManiaHitObject> ConvertHitObject(HitObject original, Beatmap beatmap)
protected override IEnumerable<ManiaHitObject> ConvertHitObject(HitObject original, IBeatmap beatmap)
{
var maniaOriginal = original as ManiaHitObject;
if (maniaOriginal != null)
@ -112,7 +113,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
/// <param name="original">The original hit object.</param>
/// <param name="originalBeatmap">The original beatmap. This is used to look-up any values dependent on a fully-loaded beatmap.</param>
/// <returns>The hit objects generated.</returns>
private IEnumerable<ManiaHitObject> generateSpecific(HitObject original, Beatmap originalBeatmap)
private IEnumerable<ManiaHitObject> generateSpecific(HitObject original, IBeatmap originalBeatmap)
{
var generator = new SpecificBeatmapPatternGenerator(random, original, beatmap, lastPattern, originalBeatmap);
@ -128,7 +129,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
/// <param name="original">The original hit object.</param>
/// <param name="originalBeatmap">The original beatmap. This is used to look-up any values dependent on a fully-loaded beatmap.</param>
/// <returns>The hit objects generated.</returns>
private IEnumerable<ManiaHitObject> generateConverted(HitObject original, Beatmap originalBeatmap)
private IEnumerable<ManiaHitObject> generateConverted(HitObject original, IBeatmap originalBeatmap)
{
var endTimeData = original as IHasEndTime;
var distanceData = original as IHasDistance;
@ -165,7 +166,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
/// </summary>
private class SpecificBeatmapPatternGenerator : Patterns.Legacy.PatternGenerator
{
public SpecificBeatmapPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, Beatmap originalBeatmap)
public SpecificBeatmapPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap)
: base(random, hitObject, beatmap, previousPattern, originalBeatmap)
{
}

View File

@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
private PatternType convertType;
public DistanceObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, Beatmap originalBeatmap)
public DistanceObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap)
: base(random, hitObject, beatmap, previousPattern, originalBeatmap)
{
convertType = PatternType.None;

View File

@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{
private readonly double endTime;
public EndTimeObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Beatmap originalBeatmap)
public EndTimeObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, IBeatmap originalBeatmap)
: base(random, hitObject, beatmap, new Pattern(), originalBeatmap)
{
var endtimeData = HitObject as IHasEndTime;

View File

@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
private readonly PatternType convertType;
public HitObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, double previousTime, Vector2 previousPosition, double density, PatternType lastStair, Beatmap originalBeatmap)
public HitObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, double previousTime, Vector2 previousPosition, double density, PatternType lastStair, IBeatmap originalBeatmap)
: base(random, hitObject, beatmap, previousPattern, originalBeatmap)
{
if (previousTime > hitObject.StartTime) throw new ArgumentOutOfRangeException(nameof(previousTime));

View File

@ -28,9 +28,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// <summary>
/// The beatmap which <see cref="HitObject"/> is being converted from.
/// </summary>
protected readonly Beatmap OriginalBeatmap;
protected readonly IBeatmap OriginalBeatmap;
protected PatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, Beatmap originalBeatmap)
protected PatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap)
: base(hitObject, beatmap, previousPattern)
{
if (random == null) throw new ArgumentNullException(nameof(random));
@ -113,7 +113,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
drainTime /= 1000;
BeatmapDifficulty difficulty = OriginalBeatmap.BeatmapInfo.BaseDifficulty;
conversionDifficulty = ((difficulty.DrainRate + MathHelper.Clamp(difficulty.ApproachRate, 4, 7)) / 1.5 + OriginalBeatmap.HitObjects.Count / drainTime * 9f) / 38f * 5f / 1.15;
conversionDifficulty = ((difficulty.DrainRate + MathHelper.Clamp(difficulty.ApproachRate, 4, 7)) / 1.5 + OriginalBeatmap.HitObjects.Count() / drainTime * 9f) / 38f * 5f / 1.15;
conversionDifficulty = Math.Min(conversionDifficulty.Value, 12);
return conversionDifficulty.Value;

View File

@ -1,16 +1,18 @@
// Copyright (c) 2007-2018 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.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mods;
using System;
using System.Collections.Generic;
namespace osu.Game.Rulesets.Mania
namespace osu.Game.Rulesets.Mania.Difficulty
{
internal class ManiaDifficultyCalculator : DifficultyCalculator<ManiaHitObject>
internal class ManiaDifficultyCalculator : DifficultyCalculator
{
private const double star_scaling_factor = 0.018;
@ -31,12 +33,12 @@ namespace osu.Game.Rulesets.Mania
/// </summary>
private readonly List<ManiaHitObjectDifficulty> difficultyHitObjects = new List<ManiaHitObjectDifficulty>();
public ManiaDifficultyCalculator(Beatmap beatmap)
public ManiaDifficultyCalculator(IBeatmap beatmap)
: base(beatmap)
{
}
public ManiaDifficultyCalculator(Beatmap beatmap, Mod[] mods)
public ManiaDifficultyCalculator(IBeatmap beatmap, Mod[] mods)
: base(beatmap, mods)
{
}
@ -48,18 +50,17 @@ namespace osu.Game.Rulesets.Mania
int columnCount = (Beatmap as ManiaBeatmap)?.TotalColumns ?? 7;
foreach (var hitObject in Beatmap.HitObjects)
difficultyHitObjects.Add(new ManiaHitObjectDifficulty(hitObject, columnCount));
// Sort DifficultyHitObjects by StartTime of the HitObjects - just to make sure.
difficultyHitObjects.Sort((a, b) => a.BaseHitObject.StartTime.CompareTo(b.BaseHitObject.StartTime));
// Note: Stable sort is done so that the ordering of hitobjects with equal start times doesn't change
difficultyHitObjects.AddRange(Beatmap.HitObjects.Select(h => new ManiaHitObjectDifficulty((ManiaHitObject)h, columnCount)).OrderBy(h => h.BaseHitObject.StartTime));
if (!calculateStrainValues())
return 0;
double starRating = calculateDifficulty() * star_scaling_factor;
categoryDifficulty?.Add("Strain", starRating);
if (categoryDifficulty != null)
categoryDifficulty["Strain"] = starRating;
return starRating;
}
@ -140,7 +141,5 @@ namespace osu.Game.Rulesets.Mania
return difficulty;
}
protected override BeatmapConverter<ManiaHitObject> CreateBeatmapConverter(Beatmap beatmap) => new ManiaBeatmapConverter(true, beatmap);
}
}

View File

@ -0,0 +1,126 @@
// Copyright (c) 2007-2018 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.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Difficulty
{
public class ManiaPerformanceCalculator : PerformanceCalculator
{
private Mod[] mods;
// Score after being scaled by non-difficulty-increasing mods
private double scaledScore;
private int countPerfect;
private int countGreat;
private int countGood;
private int countOk;
private int countMeh;
private int countMiss;
public ManiaPerformanceCalculator(Ruleset ruleset, IBeatmap beatmap, Score score)
: base(ruleset, beatmap, score)
{
}
public override double Calculate(Dictionary<string, double> categoryDifficulty = null)
{
mods = Score.Mods;
scaledScore = Score.TotalScore;
countPerfect = Convert.ToInt32(Score.Statistics[HitResult.Perfect]);
countGreat = Convert.ToInt32(Score.Statistics[HitResult.Great]);
countGood = Convert.ToInt32(Score.Statistics[HitResult.Good]);
countOk = Convert.ToInt32(Score.Statistics[HitResult.Ok]);
countMeh = Convert.ToInt32(Score.Statistics[HitResult.Meh]);
countMiss = Convert.ToInt32(Score.Statistics[HitResult.Miss]);
if (mods.Any(m => !m.Ranked))
return 0;
IEnumerable<Mod> scoreIncreaseMods = Ruleset.GetModsFor(ModType.DifficultyIncrease);
double scoreMultiplier = 1.0;
foreach (var m in mods.Where(m => !scoreIncreaseMods.Contains(m)))
scoreMultiplier *= m.ScoreMultiplier;
// Scale score up, so it's comparable to other keymods
scaledScore *= 1.0 / scoreMultiplier;
// Arbitrary initial value for scaling pp in order to standardize distributions across game modes.
// The specific number has no intrinsic meaning and can be adjusted as needed.
double multiplier = 0.8;
if (mods.Any(m => m is ModNoFail))
multiplier *= 0.9;
if (mods.Any(m => m is ModEasy))
multiplier *= 0.5;
double strainValue = computeStrainValue();
double accValue = computeAccuracyValue(strainValue);
double totalValue =
Math.Pow(
Math.Pow(strainValue, 1.1) +
Math.Pow(accValue, 1.1), 1.0 / 1.1
) * multiplier;
if (categoryDifficulty != null)
{
categoryDifficulty["Strain"] = strainValue;
categoryDifficulty["Accuracy"] = accValue;
}
return totalValue;
}
private double computeStrainValue()
{
// Obtain strain difficulty
double strainValue = Math.Pow(5 * Math.Max(1, Attributes["Strain"] / 0.2) - 4.0, 2.2) / 135.0;
// Longer maps are worth more
strainValue *= 1.0 + 0.1 * Math.Min(1.0, totalHits / 1500.0);
if (scaledScore <= 500000)
strainValue = 0;
else if (scaledScore <= 600000)
strainValue *= (scaledScore - 500000) / 100000 * 0.3;
else if (scaledScore <= 700000)
strainValue *= 0.3 + (scaledScore - 600000) / 100000 * 0.25;
else if (scaledScore <= 800000)
strainValue *= 0.55 + (scaledScore - 700000) / 100000 * 0.20;
else if (scaledScore <= 900000)
strainValue *= 0.75 + (scaledScore - 800000) / 100000 * 0.15;
else
strainValue *= 0.90 + (scaledScore - 900000) / 100000 * 0.1;
return strainValue;
}
private double computeAccuracyValue(double strainValue)
{
double hitWindowGreat = (Beatmap.HitObjects.First().HitWindows.Great / 2 - 0.5) / TimeRate;
if (hitWindowGreat <= 0)
return 0;
// Lots of arbitrary values from testing.
// Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution
double accuracyValue = Math.Max(0.0, 0.2 - (hitWindowGreat - 34) * 0.006667)
* strainValue
* Math.Pow(Math.Max(0.0, scaledScore - 960000) / 40000, 1.1);
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer
// accuracyValue *= Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3));
return accuracyValue;
}
private double totalHits => countPerfect + countOk + countGreat + countGood + countMeh + countMiss;
}
}

View File

@ -0,0 +1,13 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Judgements
{
public class HoldNoteJudgement : ManiaJudgement
{
public override bool AffectsCombo => false;
protected override int NumericResultFor(HitResult result) => 0;
}
}

View File

@ -15,12 +15,18 @@ using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Replays.Types;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Difficulty;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania
{
public class ManiaRuleset : Ruleset
{
public override RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap, bool isForCurrentRuleset) => new ManiaRulesetContainer(this, beatmap, isForCurrentRuleset);
public override RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap) => new ManiaRulesetContainer(this, beatmap);
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap);
public override PerformanceCalculator CreatePerformanceCalculator(IBeatmap beatmap, Score score) => new ManiaPerformanceCalculator(this, beatmap, score);
public override IEnumerable<Mod> ConvertLegacyMods(LegacyMods mods)
{
@ -182,7 +188,7 @@ namespace osu.Game.Rulesets.Mania
public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_mania_o };
public override DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap, Mod[] mods = null) => new ManiaDifficultyCalculator(beatmap, mods);
public override DifficultyCalculator CreateDifficultyCalculator(IBeatmap beatmap, Mod[] mods = null) => new ManiaDifficultyCalculator(beatmap, mods);
public override int? LegacyID => 3;

View File

@ -3,19 +3,18 @@
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Mania.Mods
{
public abstract class ManiaKeyMod : Mod, IApplicableToBeatmapConverter<ManiaHitObject>
public abstract class ManiaKeyMod : Mod, IApplicableToBeatmapConverter
{
public override string ShortenedName => Name;
public abstract int KeyCount { get; }
public override double ScoreMultiplier => 1; // TODO: Implement the mania key mod score multiplier
public override bool Ranked => true;
public void ApplyToBeatmapConverter(BeatmapConverter<ManiaHitObject> beatmapConverter)
public void ApplyToBeatmapConverter(IBeatmapConverter beatmapConverter)
{
var mbc = (ManiaBeatmapConverter)beatmapConverter;

View File

@ -11,19 +11,23 @@ using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModDualStages : Mod, IPlayfieldTypeMod, IApplicableToBeatmapConverter<ManiaHitObject>, IApplicableToRulesetContainer<ManiaHitObject>
public class ManiaModDualStages : Mod, IPlayfieldTypeMod, IApplicableToBeatmapConverter, IApplicableToRulesetContainer<ManiaHitObject>
{
public override string Name => "Dual Stages";
public override string ShortenedName => "DS";
public override string Description => @"Double the stages, double the fun!";
public override double ScoreMultiplier => 0;
public void ApplyToBeatmapConverter(BeatmapConverter<ManiaHitObject> beatmapConverter)
private bool isForCurrentRuleset;
public void ApplyToBeatmapConverter(IBeatmapConverter beatmapConverter)
{
var mbc = (ManiaBeatmapConverter)beatmapConverter;
isForCurrentRuleset = mbc.IsForCurrentRuleset;
// Although this can work, for now let's not allow keymods for mania-specific beatmaps
if (mbc.IsForCurrentRuleset)
if (isForCurrentRuleset)
return;
mbc.TargetColumns *= 2;
@ -34,7 +38,7 @@ namespace osu.Game.Rulesets.Mania.Mods
var mrc = (ManiaRulesetContainer)rulesetContainer;
// Although this can work, for now let's not allow keymods for mania-specific beatmaps
if (mrc.IsForCurrentRuleset)
if (isForCurrentRuleset)
return;
var newDefinitions = new List<StageDefinition>();

View File

@ -99,6 +99,19 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
protected override void UpdateState(ArmedState state)
{
switch (state)
{
case ArmedState.Hit:
// Good enough for now, we just want them to have a lifetime end
this.Delay(2000).Expire();
break;
}
}
protected override void CheckForJudgements(bool userTriggered, double timeOffset)
{
if (tail.AllJudged)
AddJudgement(new HoldNoteJudgement { Result = HitResult.Perfect });
}
protected override void Update()
@ -191,6 +204,13 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
/// </summary>
private class DrawableTailNote : DrawableNote
{
/// <summary>
/// Lenience of release hit windows. This is to make cases where the hold note release
/// is timed alongside presses of other hit objects less awkward.
/// Todo: This shouldn't exist for non-LegacyBeatmapDecoder beatmaps
/// </summary>
private const double release_window_lenience = 1.5;
private readonly DrawableHoldNote holdNote;
public DrawableTailNote(DrawableHoldNote holdNote, ManiaAction action)
@ -203,6 +223,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
protected override void CheckForJudgements(bool userTriggered, double timeOffset)
{
// Factor in the release lenience
timeOffset /= release_window_lenience;
if (!userTriggered)
{
if (!HitObject.HitWindows.CanBeHit(timeOffset))

View File

@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Mania.Objects
/// <summary>
/// The tail note of the hold.
/// </summary>
public readonly Note Tail = new TailNote();
public readonly Note Tail = new Note();
/// <summary>
/// The time between ticks of this hold.
@ -94,24 +94,5 @@ namespace osu.Game.Rulesets.Mania.Objects
});
}
}
/// <summary>
/// The tail of the hold note.
/// </summary>
private class TailNote : Note
{
/// <summary>
/// Lenience of release hit windows. This is to make cases where the hold note release
/// is timed alongside presses of other hit objects less awkward.
/// </summary>
private const double release_window_lenience = 1.5;
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
HitWindows *= release_window_lenience;
}
}
}
}

View File

@ -1,8 +1,6 @@
// Copyright (c) 2007-2018 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.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Mania.Objects.Types;
using osu.Game.Rulesets.Objects;
@ -12,12 +10,6 @@ namespace osu.Game.Rulesets.Mania.Objects
{
public virtual int Column { get; set; }
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
HitWindows.AllowsPerfect = true;
HitWindows.AllowsOk = true;
}
protected override HitWindows CreateHitWindows() => new ManiaHitWindows();
}
}

View File

@ -0,0 +1,36 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Objects
{
public class ManiaHitWindows : HitWindows
{
private static readonly IReadOnlyDictionary<HitResult, (double od0, double od5, double od10)> base_ranges = new Dictionary<HitResult, (double, double, double)>
{
{ HitResult.Perfect, (44.8, 38.8, 27.8) },
{ HitResult.Great, (128, 98, 68 ) },
{ HitResult.Good, (194, 164, 134) },
{ HitResult.Ok, (254, 224, 194) },
{ HitResult.Meh, (302, 272, 242) },
{ HitResult.Miss, (376, 346, 316) },
};
public override void SetDifficulty(double difficulty)
{
AllowsPerfect = true;
AllowsOk = true;
Perfect = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Perfect]);
Great = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Great]);
Good = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Good]);
Ok = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Ok]);
Meh = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Meh]);
Miss = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Miss]);
}
}
}

View File

@ -24,10 +24,10 @@ namespace osu.Game.Rulesets.Mania.Replays
Actions.AddRange(actions);
}
public void ConvertFrom(LegacyReplayFrame legacyFrame, Beatmap beatmap)
public void ConvertFrom(LegacyReplayFrame legacyFrame, IBeatmap beatmap)
{
// We don't need to fully convert, just create the converter
var converter = new ManiaBeatmapConverter(beatmap.BeatmapInfo.RulesetID == 3, beatmap);
var converter = new ManiaBeatmapConverter(beatmap);
// NB: Via co-op mod, osu-stable can have two stages with floor(col/2) and ceil(col/2) columns. This will need special handling
// elsewhere in the game if we do choose to support the old co-op mod anyway. For now, assume that there is only one stage.

View File

@ -36,8 +36,8 @@ namespace osu.Game.Rulesets.Mania.UI
public IEnumerable<BarLine> BarLines;
public ManiaRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap, bool isForCurrentRuleset)
: base(ruleset, beatmap, isForCurrentRuleset)
public ManiaRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap)
{
// Generate the bar lines
double lastObjectTime = (Objects.LastOrDefault() as IHasEndTime)?.EndTime ?? Objects.LastOrDefault()?.StartTime ?? double.MaxValue;
@ -85,8 +85,6 @@ namespace osu.Game.Rulesets.Mania.UI
public override PassThroughInputManager CreateInputManager() => new ManiaInputManager(Ruleset.RulesetInfo, Variant);
protected override BeatmapConverter<ManiaHitObject> CreateBeatmapConverter() => new ManiaBeatmapConverter(IsForCurrentRuleset, WorkingBeatmap.Beatmap);
protected override DrawableHitObject<ManiaHitObject> GetVisualRepresentation(ManiaHitObject h)
{
ManiaAction action = Playfield.Columns.ElementAt(h.Column).Action;

View File

@ -5,17 +5,15 @@ using System;
using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.MathUtils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Beatmaps;
using OpenTK;
namespace osu.Game.Rulesets.Osu.Tests
{
public class OsuBeatmapConversionTest : BeatmapConversionTest<ConvertValue>
internal class OsuBeatmapConversionTest : BeatmapConversionTest<ConvertValue>
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
@ -42,10 +40,10 @@ namespace osu.Game.Rulesets.Osu.Tests
};
}
protected override IBeatmapConverter CreateConverter(Beatmap beatmap) => new OsuBeatmapConverter();
protected override Ruleset CreateRuleset() => new OsuRuleset();
}
public struct ConvertValue : IEquatable<ConvertValue>
internal struct ConvertValue : IEquatable<ConvertValue>
{
/// <summary>
/// A sane value to account for osu!stable using ints everwhere.

View File

@ -93,12 +93,36 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep("Big Single, Large StackOffset", () => testSimpleBigLargeStackOffset());
AddStep("Big 1 Repeat, Large StackOffset", () => testSimpleBigLargeStackOffset(1));
AddStep("Distance Overflow", () => testDistanceOverflow());
AddStep("Distance Overflow 1 Repeat", () => testDistanceOverflow(1));
}
private void testSimpleBig(int repeats = 0) => createSlider(2, repeats: repeats);
private void testSimpleBigLargeStackOffset(int repeats = 0) => createSlider(2, repeats: repeats, stackHeight: 10);
private void testDistanceOverflow(int repeats = 0)
{
var slider = new Slider
{
StartTime = Time.Current + 1000,
Position = new Vector2(239, 176),
ControlPoints = new List<Vector2>
{
Vector2.Zero,
new Vector2(154, 28),
new Vector2(52, -34)
},
Distance = 700,
RepeatCount = repeats,
RepeatSamples = createEmptySamples(repeats),
StackHeight = 10
};
addSlider(slider, 2, 2);
}
private void testSimpleMedium(int repeats = 0) => createSlider(5, repeats: repeats);
private void testSimpleSmall(int repeats = 0) => createSlider(7, repeats: repeats);

View File

@ -0,0 +1,43 @@
// Copyright (c) 2007-2018 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.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Beatmaps
{
public class OsuBeatmap : Beatmap<OsuHitObject>
{
public override IEnumerable<BeatmapStatistic> GetStatistics()
{
int circles = HitObjects.Count(c => c is HitCircle);
int sliders = HitObjects.Count(s => s is Slider);
int spinners = HitObjects.Count(s => s is Spinner);
return new[]
{
new BeatmapStatistic
{
Name = @"Circle Count",
Content = circles.ToString(),
Icon = FontAwesome.fa_circle_o
},
new BeatmapStatistic
{
Name = @"Slider Count",
Content = sliders.ToString(),
Icon = FontAwesome.fa_circle
},
new BeatmapStatistic
{
Name = @"Spinner Count",
Content = spinners.ToString(),
Icon = FontAwesome.fa_circle
}
};
}
}
}

View File

@ -14,9 +14,14 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
{
internal class OsuBeatmapConverter : BeatmapConverter<OsuHitObject>
{
public OsuBeatmapConverter(IBeatmap beatmap)
: base(beatmap)
{
}
protected override IEnumerable<Type> ValidConversionTypes { get; } = new[] { typeof(IHasPosition) };
protected override IEnumerable<OsuHitObject> ConvertHitObject(HitObject original, Beatmap beatmap)
protected override IEnumerable<OsuHitObject> ConvertHitObject(HitObject original, IBeatmap beatmap)
{
var curveData = original as IHasCurve;
var endTimeData = original as IHasEndTime;
@ -45,8 +50,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
StartTime = original.StartTime,
Samples = original.Samples,
EndTime = endTimeData.EndTime,
Position = positionData?.Position ?? OsuPlayfield.BASE_SIZE / 2,
Position = positionData?.Position ?? OsuPlayfield.BASE_SIZE / 2
};
}
else
@ -60,5 +64,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
};
}
}
protected override Beatmap<OsuHitObject> CreateBeatmap() => new OsuBeatmap();
}
}

View File

@ -8,12 +8,17 @@ using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Beatmaps
{
internal class OsuBeatmapProcessor : BeatmapProcessor<OsuHitObject>
internal class OsuBeatmapProcessor : BeatmapProcessor
{
public override void PostProcess(Beatmap<OsuHitObject> beatmap)
public OsuBeatmapProcessor(IBeatmap beatmap)
: base(beatmap)
{
applyStacking(beatmap);
base.PostProcess(beatmap);
}
public override void PostProcess()
{
applyStacking((Beatmap<OsuHitObject>)Beatmap);
base.PostProcess();
}
private void applyStacking(Beatmap<OsuHitObject> beatmap)

View File

@ -4,55 +4,54 @@
using System;
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Difficulty.Skills;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing;
using osu.Game.Rulesets.Osu.OsuDifficulty.Skills;
namespace osu.Game.Rulesets.Osu.OsuDifficulty
namespace osu.Game.Rulesets.Osu.Difficulty
{
public class OsuDifficultyCalculator : DifficultyCalculator<OsuHitObject>
public class OsuDifficultyCalculator : DifficultyCalculator
{
private const int section_length = 400;
private const double difficulty_multiplier = 0.0675;
public OsuDifficultyCalculator(Beatmap beatmap)
public OsuDifficultyCalculator(IBeatmap beatmap)
: base(beatmap)
{
}
public OsuDifficultyCalculator(Beatmap beatmap, Mod[] mods)
public OsuDifficultyCalculator(IBeatmap beatmap, Mod[] mods)
: base(beatmap, mods)
{
}
protected override void PreprocessHitObjects()
{
new OsuBeatmapProcessor().PostProcess(Beatmap);
}
public override double Calculate(Dictionary<string, double> categoryDifficulty = null)
{
OsuDifficultyBeatmap beatmap = new OsuDifficultyBeatmap(Beatmap.HitObjects, TimeRate);
OsuDifficultyBeatmap beatmap = new OsuDifficultyBeatmap((List<OsuHitObject>)Beatmap.HitObjects, TimeRate);
Skill[] skills =
{
new Aim(),
new Speed()
};
double sectionEnd = section_length / TimeRate;
double sectionLength = section_length * TimeRate;
// The first object doesn't generate a strain, so we begin with an incremented section end
double currentSectionEnd = 2 * sectionLength;
foreach (OsuDifficultyHitObject h in beatmap)
{
while (h.BaseObject.StartTime > sectionEnd)
while (h.BaseObject.StartTime > currentSectionEnd)
{
foreach (Skill s in skills)
{
s.SaveCurrentPeak();
s.StartNewSectionFrom(sectionEnd);
s.StartNewSectionFrom(currentSectionEnd);
}
sectionEnd += section_length;
currentSectionEnd += sectionLength;
}
foreach (Skill s in skills)
@ -72,7 +71,5 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty
return starRating;
}
protected override BeatmapConverter<OsuHitObject> CreateBeatmapConverter(Beatmap beatmap) => new OsuBeatmapConverter();
}
}

View File

@ -5,35 +5,46 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Scoring
namespace osu.Game.Rulesets.Osu.Difficulty
{
public class OsuPerformanceCalculator : PerformanceCalculator<OsuHitObject>
public class OsuPerformanceCalculator : PerformanceCalculator
{
private readonly int countHitCircles;
private readonly int beatmapMaxCombo;
private Mod[] mods;
/// <summary>
/// Approach rate adjusted by mods.
/// </summary>
private double realApproachRate;
/// <summary>
/// Overall difficulty adjusted by mods.
/// </summary>
private double realOverallDifficulty;
private double accuracy;
private int scoreMaxCombo;
private int count300;
private int count100;
private int count50;
private int countGreat;
private int countGood;
private int countMeh;
private int countMiss;
public OsuPerformanceCalculator(Ruleset ruleset, Beatmap beatmap, Score score)
public OsuPerformanceCalculator(Ruleset ruleset, IBeatmap beatmap, Score score)
: base(ruleset, beatmap, score)
{
countHitCircles = Beatmap.HitObjects.Count(h => h is HitCircle);
beatmapMaxCombo = Beatmap.HitObjects.Count;
beatmapMaxCombo += Beatmap.HitObjects.OfType<Slider>().Sum(s => s.NestedHitObjects.Count) + 1;
beatmapMaxCombo = Beatmap.HitObjects.Count();
// Add the ticks + tail of the slider. 1 is subtracted because the "headcircle" would be counted twice (once for the slider itself in the line above)
beatmapMaxCombo += Beatmap.HitObjects.OfType<Slider>().Sum(s => s.NestedHitObjects.Count - 1);
}
public override double Calculate(Dictionary<string, double> categoryRatings = null)
@ -41,9 +52,9 @@ namespace osu.Game.Rulesets.Osu.Scoring
mods = Score.Mods;
accuracy = Score.Accuracy;
scoreMaxCombo = Score.MaxCombo;
count300 = Convert.ToInt32(Score.Statistics[HitResult.Great]);
count100 = Convert.ToInt32(Score.Statistics[HitResult.Good]);
count50 = Convert.ToInt32(Score.Statistics[HitResult.Meh]);
countGreat = Convert.ToInt32(Score.Statistics[HitResult.Great]);
countGood = Convert.ToInt32(Score.Statistics[HitResult.Good]);
countMeh = Convert.ToInt32(Score.Statistics[HitResult.Meh]);
countMiss = Convert.ToInt32(Score.Statistics[HitResult.Miss]);
// Don't count scores made with supposedly unranked mods
@ -58,8 +69,12 @@ namespace osu.Game.Rulesets.Osu.Scoring
ar = Math.Min(10, ar * 1.4);
if (mods.Any(m => m is OsuModEasy))
ar = Math.Max(0, ar / 2);
double preEmpt = BeatmapDifficulty.DifficultyRange(ar, 1800, 1200, 450);
double preEmpt = BeatmapDifficulty.DifficultyRange(ar, 1800, 1200, 450) / TimeRate;
double hitWindowGreat = (Beatmap.HitObjects.First().HitWindows.Great / 2 - 0.5) / TimeRate;
realApproachRate = preEmpt > 1200 ? (1800 - preEmpt) / 120 : (1200 - preEmpt) / 150 + 5;
realOverallDifficulty = (80 - 0.5 - hitWindowGreat) / 6;
// Custom multipliers for NoFail and SpunOut.
double multiplier = 1.12f; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things
@ -85,6 +100,9 @@ namespace osu.Game.Rulesets.Osu.Scoring
categoryRatings.Add("Aim", aimValue);
categoryRatings.Add("Speed", speedValue);
categoryRatings.Add("Accuracy", accuracyValue);
categoryRatings.Add("OD", realOverallDifficulty);
categoryRatings.Add("AR", realApproachRate);
categoryRatings.Add("Max Combo", beatmapMaxCombo);
}
return totalValue;
@ -121,8 +139,9 @@ namespace osu.Game.Rulesets.Osu.Scoring
aimValue *= approachRateFactor;
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
if (mods.Any(h => h is OsuModHidden))
aimValue *= 1.18f;
aimValue *= 1.02 + (11.0f - realApproachRate) / 50.0; // Gives a 1.04 bonus for AR10, a 1.06 bonus for AR9, a 1.02 bonus for AR11.
if (mods.Any(h => h is OsuModFlashlight))
{
@ -133,7 +152,7 @@ namespace osu.Game.Rulesets.Osu.Scoring
// Scale the aim value with accuracy _slightly_
aimValue *= 0.5f + accuracy / 2.0f;
// It is important to also consider accuracy difficulty when doing that
aimValue *= 0.98f + Math.Pow(Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty, 2) / 2500;
aimValue *= 0.98f + Math.Pow(realOverallDifficulty, 2) / 2500;
return aimValue;
}
@ -153,10 +172,13 @@ namespace osu.Game.Rulesets.Osu.Scoring
if (beatmapMaxCombo > 0)
speedValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8f) / Math.Pow(beatmapMaxCombo, 0.8f), 1.0f);
if (mods.Any(m => m is OsuModHidden))
speedValue *= 1.18f;
// Scale the speed value with accuracy _slightly_
speedValue *= 0.5f + accuracy / 2.0f;
// It is important to also consider accuracy difficulty when doing that
speedValue *= 0.98f + Math.Pow(Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty, 2) / 2500;
speedValue *= 0.98f + Math.Pow(realOverallDifficulty, 2) / 2500;
return speedValue;
}
@ -168,7 +190,7 @@ namespace osu.Game.Rulesets.Osu.Scoring
int amountHitObjectsWithAccuracy = countHitCircles;
if (amountHitObjectsWithAccuracy > 0)
betterAccuracyPercentage = ((count300 - (totalHits - amountHitObjectsWithAccuracy)) * 6 + count100 * 2 + count50) / (amountHitObjectsWithAccuracy * 6);
betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countGood * 2 + countMeh) / (amountHitObjectsWithAccuracy * 6);
else
betterAccuracyPercentage = 0;
@ -178,7 +200,7 @@ namespace osu.Game.Rulesets.Osu.Scoring
// Lots of arbitrary values from testing.
// Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution
double accuracyValue = Math.Pow(1.52163f, Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83f;
double accuracyValue = Math.Pow(1.52163f, realOverallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83f;
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer
accuracyValue *= Math.Min(1.15f, Math.Pow(amountHitObjectsWithAccuracy / 1000.0f, 0.3f));
@ -191,9 +213,7 @@ namespace osu.Game.Rulesets.Osu.Scoring
return accuracyValue;
}
private double totalHits => count300 + count100 + count50 + countMiss;
private double totalSuccessfulHits => count300 + count100 + count50;
protected override BeatmapConverter<OsuHitObject> CreateBeatmapConverter() => new OsuBeatmapConverter();
private double totalHits => countGreat + countGood + countMeh + countMiss;
private double totalSuccessfulHits => countGreat + countGood + countMeh;
}
}

View File

@ -0,0 +1,44 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections;
using System.Collections.Generic;
using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
{
/// <summary>
/// An enumerable container wrapping <see cref="OsuHitObject"/> input as <see cref="OsuDifficultyHitObject"/>
/// which contains extra data required for difficulty calculation.
/// </summary>
public class OsuDifficultyBeatmap : IEnumerable<OsuDifficultyHitObject>
{
private readonly IEnumerator<OsuDifficultyHitObject> difficultyObjects;
/// <summary>
/// Creates an enumerator, which preprocesses a list of <see cref="OsuHitObject"/>s recieved as input, wrapping them as
/// <see cref="OsuDifficultyHitObject"/> which contains extra data required for difficulty calculation.
/// </summary>
public OsuDifficultyBeatmap(List<OsuHitObject> objects, double timeRate)
{
// Sort OsuHitObjects by StartTime - they are not correctly ordered in some cases.
// This should probably happen before the objects reach the difficulty calculator.
objects.Sort((a, b) => a.StartTime.CompareTo(b.StartTime));
difficultyObjects = createDifficultyObjectEnumerator(objects, timeRate);
}
/// <summary>
/// Returns an enumerator that enumerates all <see cref="OsuDifficultyHitObject"/>s in the <see cref="OsuDifficultyBeatmap"/>.
/// </summary>
public IEnumerator<OsuDifficultyHitObject> GetEnumerator() => difficultyObjects;
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
private IEnumerator<OsuDifficultyHitObject> createDifficultyObjectEnumerator(List<OsuHitObject> objects, double timeRate)
{
// The first jump is formed by the first two hitobjects of the map.
// If the map has less than two OsuHitObjects, the enumerator will not return anything.
for (int i = 1; i < objects.Count; i++)
yield return new OsuDifficultyHitObject(objects[i], objects[i - 1], timeRate);
}
}
}

View File

@ -3,16 +3,18 @@
using System;
using System.Linq;
using OpenTK;
using osu.Game.Rulesets.Osu.Objects;
using OpenTK;
namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing
namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
{
/// <summary>
/// A wrapper around <see cref="OsuHitObject"/> extending it with additional data required for difficulty calculation.
/// </summary>
public class OsuDifficultyHitObject
{
private const int normalized_radius = 52;
/// <summary>
/// The <see cref="OsuHitObject"/> this <see cref="OsuDifficultyHitObject"/> refers to.
/// </summary>
@ -28,26 +30,19 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing
/// </summary>
public double DeltaTime { get; private set; }
/// <summary>
/// Number of milliseconds until the <see cref="OsuDifficultyHitObject"/> has to be hit.
/// </summary>
public double TimeUntilHit { get; set; }
private const int normalized_radius = 52;
private readonly OsuHitObject lastObject;
private readonly double timeRate;
private readonly OsuHitObject[] t;
/// <summary>
/// Initializes the object calculating extra data required for difficulty calculation.
/// </summary>
public OsuDifficultyHitObject(OsuHitObject[] triangle, double timeRate)
public OsuDifficultyHitObject(OsuHitObject currentObject, OsuHitObject lastObject, double timeRate)
{
this.lastObject = lastObject;
this.timeRate = timeRate;
t = triangle;
BaseObject = t[0];
BaseObject = currentObject;
setDistances();
setTimingValues();
// Calculate angle here
@ -63,10 +58,10 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing
scalingFactor *= 1 + smallCircleBonus;
}
Vector2 lastCursorPosition = t[1].StackedPosition;
Vector2 lastCursorPosition = lastObject.StackedPosition;
float lastTravelDistance = 0;
var lastSlider = t[1] as Slider;
var lastSlider = lastObject as Slider;
if (lastSlider != null)
{
computeSliderCursorPosition(lastSlider);
@ -80,8 +75,7 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing
private void setTimingValues()
{
// Every timing inverval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure.
DeltaTime = Math.Max(40, (t[0].StartTime - t[1].StartTime) / timeRate);
TimeUntilHit = 450; // BaseObject.PreEmpt;
DeltaTime = Math.Max(50, (BaseObject.StartTime - lastObject.StartTime) / timeRate);
}
private void computeSliderCursorPosition(Slider slider)
@ -107,7 +101,8 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing
}
});
var scoringTimes = slider.NestedHitObjects.Select(t => t.StartTime);
// Skip the head circle
var scoringTimes = slider.NestedHitObjects.Skip(1).Select(t => t.StartTime);
foreach (var time in scoringTimes)
computeVertex(time);
computeVertex(slider.EndTime);

View File

@ -2,9 +2,9 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
namespace osu.Game.Rulesets.Osu.OsuDifficulty.Skills
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{
/// <summary>
/// Represents the skill required to correctly aim at every object in the map with a uniform CircleSize and normalized distances.

View File

@ -3,11 +3,11 @@
using System;
using System.Collections.Generic;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Difficulty.Utils;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing;
using osu.Game.Rulesets.Osu.OsuDifficulty.Utils;
namespace osu.Game.Rulesets.Osu.OsuDifficulty.Skills
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{
/// <summary>
/// Used to processes strain values of <see cref="OsuDifficultyHitObject"/>s, keep track of strain levels caused by the processed objects

View File

@ -1,9 +1,9 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
namespace osu.Game.Rulesets.Osu.OsuDifficulty.Skills
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{
/// <summary>
/// Represents the skill required to press keys with regards to keeping up with the speed at which objects need to be hit.

View File

@ -5,7 +5,7 @@ using System;
using System.Collections;
using System.Collections.Generic;
namespace osu.Game.Rulesets.Osu.OsuDifficulty.Utils
namespace osu.Game.Rulesets.Osu.Difficulty.Utils
{
/// <summary>
/// An indexed stack with Push() only, which disposes items at the bottom after the capacity is full.

View File

@ -11,8 +11,8 @@ namespace osu.Game.Rulesets.Osu.Edit
{
public class OsuEditRulesetContainer : OsuRulesetContainer
{
public OsuEditRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap, bool isForCurrentRuleset)
: base(ruleset, beatmap, isForCurrentRuleset)
public OsuEditRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap)
{
}

View File

@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Edit
{
}
protected override RulesetContainer CreateRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) => new OsuEditRulesetContainer(ruleset, beatmap, true);
protected override RulesetContainer CreateRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) => new OsuEditRulesetContainer(ruleset, beatmap);
protected override IReadOnlyList<ICompositionTool> CompositionTools => new ICompositionTool[]
{

View File

@ -5,20 +5,23 @@ using System.Collections.Generic;
using System.Linq;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using OpenTK;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModHardRock : ModHardRock, IApplicableToHitObject<OsuHitObject>
public class OsuModHardRock : ModHardRock, IApplicableToHitObject
{
public override double ScoreMultiplier => 1.06;
public override bool Ranked => true;
public void ApplyToHitObject(OsuHitObject hitObject)
public void ApplyToHitObject(HitObject hitObject)
{
hitObject.Position = new Vector2(hitObject.Position.X, OsuPlayfield.BASE_SIZE.Y - hitObject.Y);
var osuObject = (OsuHitObject)hitObject;
osuObject.Position = new Vector2(osuObject.Position.X, OsuPlayfield.BASE_SIZE.Y - osuObject.Y);
var slider = hitObject as Slider;
if (slider == null)

View File

@ -71,5 +71,7 @@ namespace osu.Game.Rulesets.Osu.Objects
}
public virtual void OffsetPosition(Vector2 offset) => Position += offset;
protected override HitWindows CreateHitWindows() => new OsuHitWindows();
}
}

View File

@ -0,0 +1,29 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects
{
public class OsuHitWindows : HitWindows
{
private static readonly IReadOnlyDictionary<HitResult, (double od0, double od5, double od10)> base_ranges = new Dictionary<HitResult, (double, double, double)>
{
{ HitResult.Great, (160, 100, 40) },
{ HitResult.Good, (280, 200, 120) },
{ HitResult.Meh, (400, 300, 200) },
{ HitResult.Miss, (400, 400, 400) },
};
public override void SetDifficulty(double difficulty)
{
Great = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Great]);
Good = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Good]);
Meh = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Meh]);
Miss = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Miss]);
}
}
}

View File

@ -1,94 +0,0 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections;
using System.Collections.Generic;
using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing
{
/// <summary>
/// An enumerable container wrapping <see cref="OsuHitObject"/> input as <see cref="OsuDifficultyHitObject"/>
/// which contains extra data required for difficulty calculation.
/// </summary>
public class OsuDifficultyBeatmap : IEnumerable<OsuDifficultyHitObject>
{
private readonly IEnumerator<OsuDifficultyHitObject> difficultyObjects;
private readonly Queue<OsuDifficultyHitObject> onScreen = new Queue<OsuDifficultyHitObject>();
/// <summary>
/// Creates an enumerator, which preprocesses a list of <see cref="OsuHitObject"/>s recieved as input, wrapping them as
/// <see cref="OsuDifficultyHitObject"/> which contains extra data required for difficulty calculation.
/// </summary>
public OsuDifficultyBeatmap(List<OsuHitObject> objects, double timeRate)
{
// Sort OsuHitObjects by StartTime - they are not correctly ordered in some cases.
// This should probably happen before the objects reach the difficulty calculator.
objects.Sort((a, b) => a.StartTime.CompareTo(b.StartTime));
difficultyObjects = createDifficultyObjectEnumerator(objects, timeRate);
}
/// <summary>
/// Returns an enumerator that enumerates all <see cref="OsuDifficultyHitObject"/>s in the <see cref="OsuDifficultyBeatmap"/>.
/// The inner loop adds objects that appear on screen into a queue until we need to hit the next object.
/// The outer loop returns objects from this queue one at a time, only after they had to be hit, and should no longer be on screen.
/// This means that we can loop through every object that is on screen at the time when a new one appears,
/// allowing us to determine a reading strain for the object that just appeared.
/// </summary>
public IEnumerator<OsuDifficultyHitObject> GetEnumerator()
{
while (true)
{
// Add upcoming objects to the queue until we have at least one object that had been hit and can be dequeued.
// This means there is always at least one object in the queue unless we reached the end of the map.
do
{
if (!difficultyObjects.MoveNext())
break; // New objects can't be added anymore, but we still need to dequeue and return the ones already on screen.
OsuDifficultyHitObject latest = difficultyObjects.Current;
// Calculate flow values here
foreach (OsuDifficultyHitObject h in onScreen)
{
// ReSharper disable once PossibleNullReferenceException (resharper not smart enough to understand IEnumerator.MoveNext())
h.TimeUntilHit -= latest.DeltaTime;
// Calculate reading strain here
}
onScreen.Enqueue(latest);
}
while (onScreen.Peek().TimeUntilHit > 0); // Keep adding new objects on screen while there is still time before we have to hit the next one.
if (onScreen.Count == 0) break; // We have reached the end of the map and enumerated all the objects.
yield return onScreen.Dequeue(); // Remove and return objects one by one that had to be hit before the latest one appeared.
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
private IEnumerator<OsuDifficultyHitObject> createDifficultyObjectEnumerator(List<OsuHitObject> objects, double timeRate)
{
// We will process OsuHitObjects in groups of three to form a triangle, so we can calculate an angle for each object.
OsuHitObject[] triangle = new OsuHitObject[3];
// OsuDifficultyHitObject construction requires three components, an extra copy of the first OsuHitObject is used at the beginning.
if (objects.Count > 1)
{
triangle[1] = objects[0]; // This copy will get shifted to the last spot in the triangle.
triangle[0] = objects[0]; // This component corresponds to the real first OsuHitOject.
}
// The final component of the first triangle will be the second OsuHitOject of the map, which forms the first jump.
// If the map has less than two OsuHitObjects, the enumerator will not return anything.
for (int i = 1; i < objects.Count; ++i)
{
triangle[2] = triangle[1];
triangle[1] = triangle[0];
triangle[0] = objects[i];
yield return new OsuDifficultyHitObject(triangle, timeRate);
}
}
}
}

View File

@ -5,29 +5,29 @@ using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.OsuDifficulty;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics;
using osu.Game.Overlays.Settings;
using osu.Framework.Input.Bindings;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Osu.Edit;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Replays.Types;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Difficulty;
namespace osu.Game.Rulesets.Osu
{
public class OsuRuleset : Ruleset
{
public override RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap, bool isForCurrentRuleset) => new OsuRulesetContainer(this, beatmap, isForCurrentRuleset);
public override RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap) => new OsuRulesetContainer(this, beatmap);
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new OsuBeatmapConverter(beatmap);
public override IBeatmapProcessor CreateBeatmapProcessor(IBeatmap beatmap) => new OsuBeatmapProcessor(beatmap);
public override IEnumerable<KeyBinding> GetDefaultKeyBindings(int variant = 0) => new[]
{
@ -37,36 +37,6 @@ namespace osu.Game.Rulesets.Osu
new KeyBinding(InputKey.MouseRight, OsuAction.RightButton),
};
public override IEnumerable<BeatmapStatistic> GetBeatmapStatistics(WorkingBeatmap beatmap)
{
IEnumerable<HitObject> hitObjects = beatmap.Beatmap.HitObjects;
IEnumerable<HitObject> circles = hitObjects.Where(c => !(c is IHasEndTime));
IEnumerable<HitObject> sliders = hitObjects.Where(s => s is IHasCurve);
IEnumerable<HitObject> spinners = hitObjects.Where(s => s is IHasEndTime && !(s is IHasCurve));
return new[]
{
new BeatmapStatistic
{
Name = @"Circle Count",
Content = circles.Count().ToString(),
Icon = FontAwesome.fa_circle_o
},
new BeatmapStatistic
{
Name = @"Slider Count",
Content = sliders.Count().ToString(),
Icon = FontAwesome.fa_circle
},
new BeatmapStatistic
{
Name = @"Spinner Count",
Content = spinners.Count().ToString(),
Icon = FontAwesome.fa_circle
}
};
}
public override IEnumerable<Mod> ConvertLegacyMods(LegacyMods mods)
{
if (mods.HasFlag(LegacyMods.Nightcore))
@ -181,9 +151,9 @@ namespace osu.Game.Rulesets.Osu
public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_osu_o };
public override DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap, Mod[] mods = null) => new OsuDifficultyCalculator(beatmap, mods);
public override DifficultyCalculator CreateDifficultyCalculator(IBeatmap beatmap, Mod[] mods = null) => new OsuDifficultyCalculator(beatmap, mods);
public override PerformanceCalculator CreatePerformanceCalculator(Beatmap beatmap, Score score) => new OsuPerformanceCalculator(this, beatmap, score);
public override PerformanceCalculator CreatePerformanceCalculator(IBeatmap beatmap, Score score) => new OsuPerformanceCalculator(this, beatmap, score);
public override HitObjectComposer CreateHitObjectComposer() => new OsuHitObjectComposer(this);

View File

@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Replays
Actions.AddRange(actions);
}
public void ConvertFrom(LegacyReplayFrame legacyFrame, Beatmap beatmap)
public void ConvertFrom(LegacyReplayFrame legacyFrame, IBeatmap beatmap)
{
Position = legacyFrame.Position;
if (legacyFrame.MouseLeft) Actions.Add(OsuAction.LeftButton);

View File

@ -7,7 +7,6 @@ using OpenTK;
using osu.Game.Beatmaps;
using osu.Game.Input.Handlers;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Replays;
@ -21,17 +20,13 @@ namespace osu.Game.Rulesets.Osu.UI
{
public class OsuRulesetContainer : RulesetContainer<OsuHitObject>
{
public OsuRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap, bool isForCurrentRuleset)
: base(ruleset, beatmap, isForCurrentRuleset)
public OsuRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap)
{
}
public override ScoreProcessor CreateScoreProcessor() => new OsuScoreProcessor(this);
protected override BeatmapConverter<OsuHitObject> CreateBeatmapConverter() => new OsuBeatmapConverter();
protected override BeatmapProcessor<OsuHitObject> CreateBeatmapProcessor() => new OsuBeatmapProcessor();
protected override Playfield CreatePlayfield() => new OsuPlayfield();
public override PassThroughInputManager CreateInputManager() => new OsuInputManager(Ruleset.RulesetInfo);

View File

@ -5,27 +5,22 @@ using System;
using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.MathUtils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Taiko.Beatmaps;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Taiko.Tests
{
public class TaikoBeatmapConversionTest : BeatmapConversionTest<ConvertValue>
internal class TaikoBeatmapConversionTest : BeatmapConversionTest<ConvertValue>
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
private bool isForCurrentRuleset;
[NonParallelizable]
[TestCase("basic", false), Ignore("See: https://github.com/ppy/osu/issues/2152")]
[TestCase("slider-generating-drumroll", false)]
public void Test(string name, bool isForCurrentRuleset)
[TestCase("basic")]
[TestCase("slider-generating-drumroll")]
public new void Test(string name)
{
this.isForCurrentRuleset = isForCurrentRuleset;
base.Test(name);
}
@ -43,10 +38,10 @@ namespace osu.Game.Rulesets.Taiko.Tests
};
}
protected override IBeatmapConverter CreateConverter(Beatmap beatmap) => new TaikoBeatmapConverter(isForCurrentRuleset);
protected override Ruleset CreateRuleset() => new TaikoRuleset();
}
public struct ConvertValue : IEquatable<ConvertValue>
internal struct ConvertValue : IEquatable<ConvertValue>
{
/// <summary>
/// A sane value to account for osu!stable using ints everwhere.

View File

@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
private Container playfieldContainer;
[BackgroundDependencyLoader]
private void load(RulesetStore rulesets)
private void load()
{
AddStep("Hit!", () => addHitJudgement(false));
AddStep("Kiai hit", () => addHitJudgement(true));
@ -73,6 +73,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
Title = @"Sample Beatmap",
AuthorString = @"peppy",
},
Ruleset = new TaikoRuleset().RulesetInfo
},
ControlPointInfo = controlPointInfo
});
@ -86,7 +87,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
RelativeSizeAxes = Axes.X,
Height = 768,
Clock = new FramedClock(rateAdjustClock),
Children = new[] { rulesetContainer = new TaikoRulesetContainer(rulesets.GetRuleset(1).CreateInstance(), beatmap, true) }
Children = new[] { rulesetContainer = new TaikoRulesetContainer(new TaikoRuleset(), beatmap) }
});
}

View File

@ -0,0 +1,43 @@
// Copyright (c) 2007-2018 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.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Rulesets.Taiko.Beatmaps
{
public class TaikoBeatmap : Beatmap<TaikoHitObject>
{
public override IEnumerable<BeatmapStatistic> GetStatistics()
{
int hits = HitObjects.Count(s => s is Hit);
int drumrolls = HitObjects.Count(s => s is DrumRoll);
int swells = HitObjects.Count(s => s is Swell);
return new[]
{
new BeatmapStatistic
{
Name = @"Hit Count",
Content = hits.ToString(),
Icon = FontAwesome.fa_circle_o
},
new BeatmapStatistic
{
Name = @"Drumroll Count",
Content = drumrolls.ToString(),
Icon = FontAwesome.fa_circle
},
new BeatmapStatistic
{
Name = @"Swell Count",
Content = swells.ToString(),
Icon = FontAwesome.fa_circle
}
};
}
}
}

View File

@ -8,7 +8,6 @@ using osu.Game.Rulesets.Taiko.Objects;
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.IO.Serialization;
using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints;
@ -42,16 +41,18 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
protected override IEnumerable<Type> ValidConversionTypes { get; } = new[] { typeof(HitObject) };
public TaikoBeatmapConverter(bool isForCurrentRuleset)
public TaikoBeatmapConverter(IBeatmap beatmap)
: base(beatmap)
{
this.isForCurrentRuleset = isForCurrentRuleset;
isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(new TaikoRuleset().RulesetInfo);
}
protected override Beatmap<TaikoHitObject> ConvertBeatmap(Beatmap original)
protected override Beatmap<TaikoHitObject> ConvertBeatmap(IBeatmap original)
{
// Rewrite the beatmap info to add the slider velocity multiplier
BeatmapInfo info = original.BeatmapInfo.DeepClone();
info.BaseDifficulty.SliderMultiplier *= legacy_velocity_multiplier;
original.BeatmapInfo = original.BeatmapInfo.Clone();
original.BeatmapInfo.BaseDifficulty = original.BeatmapInfo.BaseDifficulty.Clone();
original.BeatmapInfo.BaseDifficulty.SliderMultiplier *= legacy_velocity_multiplier;
Beatmap<TaikoHitObject> converted = base.ConvertBeatmap(original);
@ -70,7 +71,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
return converted;
}
protected override IEnumerable<TaikoHitObject> ConvertHitObject(HitObject obj, Beatmap beatmap)
protected override IEnumerable<TaikoHitObject> ConvertHitObject(HitObject obj, IBeatmap beatmap)
{
var distanceData = obj as IHasDistance;
var repeatsData = obj as IHasRepeats;
@ -97,12 +98,12 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
double distance = distanceData.Distance * spans * legacy_velocity_multiplier;
// The velocity of the taiko hit object - calculated as the velocity of a drum roll
double taikoVelocity = taiko_base_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier * legacy_velocity_multiplier / speedAdjustedBeatLength;
double taikoVelocity = taiko_base_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier / speedAdjustedBeatLength;
// The duration of the taiko hit object
double taikoDuration = distance / taikoVelocity;
// The velocity of the osu! hit object - calculated as the velocity of a slider
double osuVelocity = osu_base_scoring_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier * legacy_velocity_multiplier / speedAdjustedBeatLength;
double osuVelocity = osu_base_scoring_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier / speedAdjustedBeatLength;
// The duration of the osu! hit object
double osuDuration = distance / osuVelocity;
@ -140,7 +141,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
{
StartTime = j,
Samples = currentSamples,
IsStrong = strong,
IsStrong = strong
};
}
@ -155,7 +156,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
Samples = obj.Samples,
IsStrong = strong,
Duration = taikoDuration,
TickRate = beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate == 3 ? 3 : 4,
TickRate = beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate == 3 ? 3 : 4
};
}
}
@ -169,7 +170,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
Samples = obj.Samples,
IsStrong = strong,
Duration = endTimeData.Duration,
RequiredHits = (int)Math.Max(1, endTimeData.Duration / 1000 * hitMultiplier),
RequiredHits = (int)Math.Max(1, endTimeData.Duration / 1000 * hitMultiplier)
};
}
else
@ -182,7 +183,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
{
StartTime = obj.StartTime,
Samples = obj.Samples,
IsStrong = strong,
IsStrong = strong
};
}
else
@ -191,10 +192,12 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
{
StartTime = obj.StartTime,
Samples = obj.Samples,
IsStrong = strong,
IsStrong = strong
};
}
}
}
protected override Beatmap<TaikoHitObject> CreateBeatmap() => new TaikoBeatmap();
}
}

View File

@ -1,15 +1,16 @@
// Copyright (c) 2007-2018 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.Taiko.Beatmaps;
using osu.Game.Rulesets.Taiko.Objects;
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Rulesets.Taiko
namespace osu.Game.Rulesets.Taiko.Difficulty
{
internal class TaikoDifficultyCalculator : DifficultyCalculator<TaikoHitObject>
internal class TaikoDifficultyCalculator : DifficultyCalculator
{
private const double star_scaling_factor = 0.04125;
@ -30,18 +31,23 @@ namespace osu.Game.Rulesets.Taiko
/// </summary>
private readonly List<TaikoHitObjectDifficulty> difficultyHitObjects = new List<TaikoHitObjectDifficulty>();
public TaikoDifficultyCalculator(Beatmap beatmap)
public TaikoDifficultyCalculator(IBeatmap beatmap)
: base(beatmap)
{
}
public TaikoDifficultyCalculator(IBeatmap beatmap, Mod[] mods)
: base(beatmap, mods)
{
}
public override double Calculate(Dictionary<string, double> categoryDifficulty = null)
{
// Fill our custom DifficultyHitObject class, that carries additional information
difficultyHitObjects.Clear();
foreach (var hitObject in Beatmap.HitObjects)
difficultyHitObjects.Add(new TaikoHitObjectDifficulty(hitObject));
difficultyHitObjects.Add(new TaikoHitObjectDifficulty((TaikoHitObject)hitObject));
// Sort DifficultyHitObjects by StartTime of the HitObjects - just to make sure.
difficultyHitObjects.Sort((a, b) => a.BaseHitObject.StartTime.CompareTo(b.BaseHitObject.StartTime));
@ -51,10 +57,7 @@ namespace osu.Game.Rulesets.Taiko
double starRating = calculateDifficulty() * star_scaling_factor;
if (categoryDifficulty != null)
{
categoryDifficulty.Add("Strain", starRating);
categoryDifficulty.Add("Hit window 300", 35 /*HitObjectManager.HitWindow300*/ / TimeRate);
}
categoryDifficulty["Strain"] = starRating;
return starRating;
}
@ -132,7 +135,5 @@ namespace osu.Game.Rulesets.Taiko
return difficulty;
}
protected override BeatmapConverter<TaikoHitObject> CreateBeatmapConverter(Beatmap beatmap) => new TaikoBeatmapConverter(true);
}
}

View File

@ -0,0 +1,111 @@
// Copyright (c) 2007-2018 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.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Rulesets.Taiko.Difficulty
{
public class TaikoPerformanceCalculator : PerformanceCalculator
{
private readonly int beatmapMaxCombo;
private Mod[] mods;
private int countGreat;
private int countGood;
private int countMeh;
private int countMiss;
public TaikoPerformanceCalculator(Ruleset ruleset, IBeatmap beatmap, Score score)
: base(ruleset, beatmap, score)
{
beatmapMaxCombo = beatmap.HitObjects.Count(h => h is Hit);
}
public override double Calculate(Dictionary<string, double> categoryDifficulty = null)
{
mods = Score.Mods;
countGreat = Convert.ToInt32(Score.Statistics[HitResult.Great]);
countGood = Convert.ToInt32(Score.Statistics[HitResult.Good]);
countMeh = Convert.ToInt32(Score.Statistics[HitResult.Meh]);
countMiss = Convert.ToInt32(Score.Statistics[HitResult.Miss]);
// Don't count scores made with supposedly unranked mods
if (mods.Any(m => !m.Ranked))
return 0;
// Custom multipliers for NoFail and SpunOut.
double multiplier = 1.1; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things
if (mods.Any(m => m is ModNoFail))
multiplier *= 0.90;
if (mods.Any(m => m is ModHidden))
multiplier *= 1.10;
double strainValue = computeStrainValue();
double accuracyValue = computeAccuracyValue();
double totalValue =
Math.Pow(
Math.Pow(strainValue, 1.1) +
Math.Pow(accuracyValue, 1.1), 1.0 / 1.1
) * multiplier;
if (categoryDifficulty != null)
{
categoryDifficulty["Strain"] = strainValue;
categoryDifficulty["Accuracy"] = accuracyValue;
}
return totalValue;
}
private double computeStrainValue()
{
double strainValue = Math.Pow(5.0 * Math.Max(1.0, Attributes["Strain"] / 0.0075) - 4.0, 2.0) / 100000.0;
// Longer maps are worth more
double lengthBonus = 1 + 0.1f * Math.Min(1.0, totalHits / 1500.0);
strainValue *= lengthBonus;
// Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available
strainValue *= Math.Pow(0.985, countMiss);
// Combo scaling
if (beatmapMaxCombo > 0)
strainValue *= Math.Min(Math.Pow(Score.MaxCombo, 0.5) / Math.Pow(beatmapMaxCombo, 0.5), 1.0);
if (mods.Any(m => m is ModHidden))
strainValue *= 1.025;
if (mods.Any(m => m is ModFlashlight))
// Apply length bonus again if flashlight is on simply because it becomes a lot harder on longer maps.
strainValue *= 1.05 * lengthBonus;
// Scale the speed value with accuracy _slightly_
return strainValue * Score.Accuracy;
}
private double computeAccuracyValue()
{
double hitWindowGreat = (Beatmap.HitObjects.First().HitWindows.Great / 2 - 0.5) / TimeRate;
if (hitWindowGreat <= 0)
return 0;
// Lots of arbitrary values from testing.
// Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution
double accValue = Math.Pow(150.0 / hitWindowGreat, 1.1) * Math.Pow(Score.Accuracy, 15) * 22.0;
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer
return accValue * Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3));
}
private int totalHits => countGreat + countGood + countMeh + countMiss;
}
}

View File

@ -40,6 +40,8 @@ namespace osu.Game.Rulesets.Taiko.Objects
/// </summary>
private double tickSpacing = 100;
private float overallDifficulty = BeatmapDifficulty.DEFAULT_DIFFICULTY;
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
@ -47,9 +49,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
tickSpacing = timingPoint.BeatLength / TickRate;
RequiredGoodHits = NestedHitObjects.Count * Math.Min(0.15, 0.05 + 0.10 / 6 * difficulty.OverallDifficulty);
RequiredGreatHits = NestedHitObjects.Count * Math.Min(0.30, 0.10 + 0.20 / 6 * difficulty.OverallDifficulty);
overallDifficulty = difficulty.OverallDifficulty;
}
protected override void CreateNestedHitObjects()
@ -57,6 +57,9 @@ namespace osu.Game.Rulesets.Taiko.Objects
base.CreateNestedHitObjects();
createTicks();
RequiredGoodHits = NestedHitObjects.Count * Math.Min(0.15, 0.05 + 0.10 / 6 * overallDifficulty);
RequiredGreatHits = NestedHitObjects.Count * Math.Min(0.30, 0.10 + 0.20 / 6 * overallDifficulty);
}
private void createTicks()

View File

@ -27,5 +27,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
/// Strong hit objects give more points for hitting the hit object with both keys.
/// </summary>
public bool IsStrong;
protected override HitWindows CreateHitWindows() => new TaikoHitWindows();
}
}

View File

@ -0,0 +1,29 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Taiko.Objects
{
public class TaikoHitWindows : HitWindows
{
private static readonly IReadOnlyDictionary<HitResult, (double od0, double od5, double od10)> base_ranges = new Dictionary<HitResult, (double, double, double)>
{
{ HitResult.Great, (100, 70, 40) },
{ HitResult.Good, (240, 160, 100) },
{ HitResult.Meh, (270, 190, 140) },
{ HitResult.Miss, (400, 400, 400) },
};
public override void SetDifficulty(double difficulty)
{
Great = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Great]);
Good = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Good]);
Meh = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Meh]);
Miss = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Miss]);
}
}
}

View File

@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Taiko.Replays
Actions.AddRange(actions);
}
public void ConvertFrom(LegacyReplayFrame legacyFrame, Beatmap beatmap)
public void ConvertFrom(LegacyReplayFrame legacyFrame, IBeatmap beatmap)
{
if (legacyFrame.MouseRight1) Actions.Add(TaikoAction.LeftRim);
if (legacyFrame.MouseRight2) Actions.Add(TaikoAction.RightRim);

View File

@ -13,12 +13,17 @@ using osu.Framework.Input.Bindings;
using osu.Game.Rulesets.Replays.Types;
using osu.Game.Rulesets.Taiko.Replays;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Beatmaps;
using osu.Game.Rulesets.Taiko.Difficulty;
namespace osu.Game.Rulesets.Taiko
{
public class TaikoRuleset : Ruleset
{
public override RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap, bool isForCurrentRuleset) => new TaikoRulesetContainer(this, beatmap, isForCurrentRuleset);
public override RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap) => new TaikoRulesetContainer(this, beatmap);
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new TaikoBeatmapConverter(beatmap);
public override IEnumerable<KeyBinding> GetDefaultKeyBindings(int variant = 0) => new[]
{
@ -140,7 +145,9 @@ namespace osu.Game.Rulesets.Taiko
public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_taiko_o };
public override DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap, Mod[] mods = null) => new TaikoDifficultyCalculator(beatmap);
public override DifficultyCalculator CreateDifficultyCalculator(IBeatmap beatmap, Mod[] mods = null) => new TaikoDifficultyCalculator(beatmap, mods);
public override PerformanceCalculator CreatePerformanceCalculator(IBeatmap beatmap, Score score) => new TaikoPerformanceCalculator(this, beatmap, score);
public override int? LegacyID => 1;

View File

@ -8,7 +8,6 @@ using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Beatmaps;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Scoring;
@ -24,8 +23,8 @@ namespace osu.Game.Rulesets.Taiko.UI
{
public class TaikoRulesetContainer : ScrollingRulesetContainer<TaikoPlayfield, TaikoHitObject>
{
public TaikoRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap, bool isForCurrentRuleset)
: base(ruleset, beatmap, isForCurrentRuleset)
public TaikoRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap)
{
}
@ -93,8 +92,6 @@ namespace osu.Game.Rulesets.Taiko.UI
public override ScoreProcessor CreateScoreProcessor() => new TaikoScoreProcessor(this);
protected override BeatmapConverter<TaikoHitObject> CreateBeatmapConverter() => new TaikoBeatmapConverter(IsForCurrentRuleset);
public override PassThroughInputManager CreateInputManager() => new TaikoInputManager(Ruleset.RulesetInfo);
protected override Playfield CreatePlayfield() => new TaikoPlayfield(Beatmap.ControlPointInfo)

View File

@ -9,6 +9,7 @@ using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats;
using osu.Game.IO.Serialization;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Tests.Resources;
using OpenTK;
@ -117,7 +118,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
public void TestParity(string beatmap)
{
var legacy = decode(beatmap, out Beatmap json);
json.ShouldDeepEqual(legacy);
json.WithDeepEqual(legacy).IgnoreProperty(r => r.DeclaringType == typeof(HitWindows)).Assert();
}
/// <summary>

View File

@ -275,13 +275,13 @@ namespace osu.Game.Tests.Beatmaps.IO
Assert.IsTrue(set.Beatmaps.Any(c => c.OnlineBeatmapID == b.OnlineBeatmapID));
Assert.IsTrue(set.Beatmaps.Count > 0);
var beatmap = store.GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 0))?.Beatmap;
Assert.IsTrue(beatmap?.HitObjects.Count > 0);
Assert.IsTrue(beatmap?.HitObjects.Any() == true);
beatmap = store.GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 1))?.Beatmap;
Assert.IsTrue(beatmap?.HitObjects.Count > 0);
Assert.IsTrue(beatmap?.HitObjects.Any() == true);
beatmap = store.GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 2))?.Beatmap;
Assert.IsTrue(beatmap?.HitObjects.Count > 0);
Assert.IsTrue(beatmap?.HitObjects.Any() == true);
beatmap = store.GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 3))?.Beatmap;
Assert.IsTrue(beatmap?.HitObjects.Count > 0);
Assert.IsTrue(beatmap?.HitObjects.Any() == true);
}
private void waitForOrAssert(Func<bool> result, string failureMessage, int timeout = 60000)

View File

@ -12,8 +12,12 @@ using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Taiko;
using osu.Game.Screens.Select;
using osu.Game.Tests.Beatmaps;
@ -24,7 +28,7 @@ namespace osu.Game.Tests.Visual
{
private RulesetStore rulesets;
private TestBeatmapInfoWedge infoWedge;
private readonly List<Beatmap> beatmaps = new List<Beatmap>();
private readonly List<IBeatmap> beatmaps = new List<IBeatmap>();
private readonly Bindable<WorkingBeatmap> beatmap = new Bindable<WorkingBeatmap>();
[BackgroundDependencyLoader]
@ -72,13 +76,23 @@ namespace osu.Game.Tests.Visual
selectBeatmap(testBeatmap);
testBeatmapLabels(ruleset);
// TODO: adjust cases once more info is shown for other gamemodes
switch (ruleset)
{
case OsuRuleset osu:
testOsuBeatmap(osu);
case OsuRuleset _:
testInfoLabels(5);
break;
case TaikoRuleset _:
testInfoLabels(5);
break;
case CatchRuleset _:
testInfoLabels(5);
break;
case ManiaRuleset _:
testInfoLabels(4);
break;
default:
testInfoLabels(2);
break;
@ -88,7 +102,7 @@ namespace osu.Game.Tests.Visual
testNullBeatmap();
}
private void testOsuBeatmap(OsuRuleset ruleset)
private void testBeatmapLabels(Ruleset ruleset)
{
AddAssert("check version", () => infoWedge.Info.VersionLabel.Text == $"{ruleset.ShortName}Version");
AddAssert("check title", () => infoWedge.Info.TitleLabel.Text == $"{ruleset.ShortName}Source — {ruleset.ShortName}Title");
@ -112,7 +126,7 @@ namespace osu.Game.Tests.Visual
AddAssert("check no infolabels", () => !infoWedge.Info.InfoLabelContainer.Children.Any());
}
private void selectBeatmap(Beatmap b)
private void selectBeatmap(IBeatmap b)
{
BeatmapInfoWedge.BufferedWedgeInfo infoBefore = null;
@ -134,11 +148,11 @@ namespace osu.Game.Tests.Visual
});
}
private Beatmap createTestBeatmap(RulesetInfo ruleset)
private IBeatmap createTestBeatmap(RulesetInfo ruleset)
{
List<HitObject> objects = new List<HitObject>();
for (double i = 0; i < 50000; i += 1000)
objects.Add(new HitObject { StartTime = i });
objects.Add(new TestHitObject { StartTime = i });
return new Beatmap
{
@ -153,7 +167,8 @@ namespace osu.Game.Tests.Visual
},
Ruleset = ruleset,
StarDifficulty = 6,
Version = $"{ruleset.ShortName}Version"
Version = $"{ruleset.ShortName}Version",
BaseDifficulty = new BeatmapDifficulty()
},
HitObjects = objects
};
@ -163,5 +178,12 @@ namespace osu.Game.Tests.Visual
{
public new BufferedWedgeInfo Info => base.Info;
}
private class TestHitObject : HitObject, IHasPosition
{
public float X { get; } = 0;
public float Y { get; } = 0;
public Vector2 Position { get; } = Vector2.Zero;
}
}
}

View File

@ -1,7 +1,6 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Timing;
using osu.Game.Beatmaps.Timing;
using System.Collections.Generic;
using NUnit.Framework;
@ -16,8 +15,6 @@ namespace osu.Game.Tests.Visual
public TestCaseBreakOverlay()
{
Clock = new FramedClock();
Child = breakOverlay = new BreakOverlay(true);
AddStep("2s break", () => startBreak(2000));

View File

@ -6,9 +6,10 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets;
using osu.Game.Screens.Multiplayer;
using osu.Game.Screens.Multi.Components;
using osu.Game.Users;
namespace osu.Game.Tests.Visual
@ -111,6 +112,7 @@ namespace osu.Game.Tests.Visual
}
});
AddStep(@"select", () => first.State = SelectionState.Selected);
AddStep(@"change title", () => first.Room.Name.Value = @"I Changed Name");
AddStep(@"change host", () => first.Room.Host.Value = new User { Username = @"DrabWeb", Id = 6946022, Country = new Country { FlagName = @"CA" } });
AddStep(@"change status", () => first.Room.Status.Value = new RoomStatusPlaying());
@ -121,6 +123,7 @@ namespace osu.Game.Tests.Visual
new User { Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 1254 } } },
new User { Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 123189 } } },
});
AddStep(@"deselect", () => first.State = SelectionState.NotSelected);
}
[BackgroundDependencyLoader]

View File

@ -332,7 +332,7 @@ namespace osu.Game.Tests.Visual
private readonly Drawable tracker;
public TimingPointVisualiser(Beatmap beatmap, double length)
public TimingPointVisualiser(IBeatmap beatmap, double length)
{
this.length = length;

View File

@ -0,0 +1,62 @@
// Copyright (c) 2007-2018 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 osu.Framework.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Screens.Menu;
namespace osu.Game.Tests.Visual
{
public class TestCaseHoldToConfirmOverlay : OsuTestCase
{
public override IReadOnlyList<Type> RequiredTypes => new[] { typeof(ExitConfirmOverlay) };
public TestCaseHoldToConfirmOverlay()
{
bool fired = false;
var firedText = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = "Fired!",
TextSize = 50,
Alpha = 0,
};
var overlay = new TestHoldToConfirmOverlay
{
Action = () =>
{
fired = true;
firedText.FadeTo(1).Then().FadeOut(1000);
}
};
Children = new Drawable[]
{
overlay,
firedText
};
AddStep("start confirming", () => overlay.Begin());
AddStep("abort confirming", () => overlay.Abort());
AddAssert("ensure aborted", () => !fired);
AddStep("start confirming", () => overlay.Begin());
AddUntilStep(() => fired, "wait until confirmed");
}
private class TestHoldToConfirmOverlay : ExitConfirmOverlay
{
protected override bool AllowMultipleFires => true;
public void Begin() => BeginConfirm();
public void Abort() => AbortConfirm();
}
}
}

View File

@ -1,6 +1,7 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.ComponentModel;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
@ -17,6 +18,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.UI;
using OpenTK.Graphics;
namespace osu.Game.Tests.Visual
@ -24,6 +26,19 @@ namespace osu.Game.Tests.Visual
[Description("mod select and icon display")]
public class TestCaseMods : OsuTestCase
{
public override IReadOnlyList<Type> RequiredTypes => new[]
{
typeof(ModSelectOverlay),
typeof(ModDisplay),
typeof(ModSection),
typeof(ModIcon),
typeof(ModButton),
typeof(ModButtonEmpty),
typeof(DifficultyReductionSection),
typeof(DifficultyIncreaseSection),
typeof(SpecialSection),
};
private const string unranked_suffix = " (Unranked)";
private RulesetStore rulesets;
@ -66,7 +81,8 @@ namespace osu.Game.Tests.Visual
Ruleset ruleset = rulesetInfo.CreateInstance();
AddStep($"switch to {ruleset.Description}", () => modSelect.Ruleset.Value = rulesetInfo);
switch (ruleset) {
switch (ruleset)
{
case OsuRuleset or:
testOsuMods(or);
break;

View File

@ -0,0 +1,27 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Screens.Multi;
using osu.Game.Screens.Multi.Screens;
namespace osu.Game.Tests.Visual
{
[TestFixture]
public class TestCaseMultiHeader : OsuTestCase
{
public TestCaseMultiHeader()
{
Lobby lobby;
Children = new Drawable[]
{
lobby = new Lobby
{
Padding = new MarginPadding { Top = Header.HEIGHT },
},
new Header(lobby),
};
}
}
}

View File

@ -0,0 +1,21 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using NUnit.Framework;
using osu.Game.Screens.Multi;
namespace osu.Game.Tests.Visual
{
[TestFixture]
public class TestCaseMultiScreen : OsuTestCase
{
public TestCaseMultiScreen()
{
Multiplayer multi = new Multiplayer();
AddStep(@"show", () => Add(multi));
AddWaitStep(5);
AddStep(@"exit", multi.Exit);
}
}
}

View File

@ -4,6 +4,8 @@
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Configuration.Tracking;
using osu.Framework.Graphics;
using osu.Game.Overlays;
namespace osu.Game.Tests.Visual
@ -11,36 +13,82 @@ namespace osu.Game.Tests.Visual
[TestFixture]
public class TestCaseOnScreenDisplay : OsuTestCase
{
private FrameworkConfigManager config;
private Bindable<FrameSync> frameSyncMode;
protected override void LoadComplete()
{
base.LoadComplete();
Add(new OnScreenDisplay());
frameSyncMode = config.GetBindable<FrameSync>(FrameworkSetting.FrameSync);
FrameSync initial = frameSyncMode.Value;
AddRepeatStep(@"Change frame limiter", setNextMode, 3);
AddStep(@"Restore frame limiter", () => frameSyncMode.Value = initial);
}
private void setNextMode()
{
var nextMode = frameSyncMode.Value + 1;
if (nextMode > FrameSync.Unlimited)
nextMode = FrameSync.VSync;
frameSyncMode.Value = nextMode;
}
[BackgroundDependencyLoader]
private void load(FrameworkConfigManager config)
private void load()
{
this.config = config;
var config = new TestConfigManager();
var osd = new TestOnScreenDisplay();
osd.BeginTracking(this, config);
Add(osd);
AddRepeatStep("Change toggle (no bind)", () => config.ToggleSetting(TestConfigSetting.ToggleSettingNoKeybind), 2);
AddRepeatStep("Change toggle (with bind)", () => config.ToggleSetting(TestConfigSetting.ToggleSettingWithKeybind), 2);
AddRepeatStep("Change enum (no bind)", () => config.IncrementEnumSetting(TestConfigSetting.EnumSettingNoKeybind), 3);
AddRepeatStep("Change enum (with bind)", () => config.IncrementEnumSetting(TestConfigSetting.EnumSettingWithKeybind), 3);
}
private class TestConfigManager : ConfigManager<TestConfigSetting>
{
public TestConfigManager()
{
InitialiseDefaults();
}
protected override void InitialiseDefaults()
{
Set(TestConfigSetting.ToggleSettingNoKeybind, false);
Set(TestConfigSetting.EnumSettingNoKeybind, EnumSetting.Setting1);
Set(TestConfigSetting.ToggleSettingWithKeybind, false);
Set(TestConfigSetting.EnumSettingWithKeybind, EnumSetting.Setting1);
base.InitialiseDefaults();
}
public void ToggleSetting(TestConfigSetting setting) => Set(setting, !Get<bool>(setting));
public void IncrementEnumSetting(TestConfigSetting setting)
{
var nextValue = Get<EnumSetting>(setting) + 1;
if (nextValue > EnumSetting.Setting4)
nextValue = EnumSetting.Setting1;
Set(setting, nextValue);
}
public override TrackedSettings CreateTrackedSettings() => new TrackedSettings
{
new TrackedSetting<bool>(TestConfigSetting.ToggleSettingNoKeybind, b => new SettingDescription(b, "toggle setting with no keybind", b ? "enabled" : "disabled")),
new TrackedSetting<EnumSetting>(TestConfigSetting.EnumSettingNoKeybind, v => new SettingDescription(v, "enum setting with no keybind", v.ToString())),
new TrackedSetting<bool>(TestConfigSetting.ToggleSettingWithKeybind, b => new SettingDescription(b, "toggle setting with keybind", b ? "enabled" : "disabled", "fake keybind")),
new TrackedSetting<EnumSetting>(TestConfigSetting.EnumSettingWithKeybind, v => new SettingDescription(v, "enum setting with keybind", v.ToString(), "fake keybind")),
};
protected override void PerformLoad()
{
}
protected override bool PerformSave() => false;
}
private enum TestConfigSetting
{
ToggleSettingNoKeybind,
EnumSettingNoKeybind,
ToggleSettingWithKeybind,
EnumSettingWithKeybind
}
private enum EnumSetting
{
Setting1,
Setting2,
Setting3,
Setting4
}
private class TestOnScreenDisplay : OnScreenDisplay
{
protected override void Display(Drawable toDisplay) => toDisplay.FadeIn().ResizeHeightTo(110);
}
}
}

View File

@ -18,7 +18,7 @@ namespace osu.Game.Tests.Visual
// We create a dummy RulesetContainer just to get the replay - we don't want to use mods here
// to simulate setting a replay rather than having the replay already set for us
beatmap.Mods.Value = beatmap.Mods.Value.Concat(new[] { ruleset.GetAutoplayMod() });
var dummyRulesetContainer = ruleset.CreateRulesetContainerWith(beatmap, beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo));
var dummyRulesetContainer = ruleset.CreateRulesetContainerWith(beatmap);
// We have the replay
var replay = dummyRulesetContainer.Replay;

View File

@ -7,7 +7,7 @@ using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets;
using osu.Game.Screens.Multiplayer;
using osu.Game.Screens.Multi.Components;
using osu.Game.Users;
namespace osu.Game.Tests.Visual
@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual
{
base.LoadComplete();
var room = new Room
Room room = new Room
{
Name = { Value = @"My Awesome Room" },
Host = { Value = new User { Username = @"flyte", Id = 3103765, Country = new Country { FlagName = @"JP" } } },
@ -71,9 +71,13 @@ namespace osu.Game.Tests.Visual
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Room = room,
RelativeSizeAxes = Axes.Both,
Width = 0.5f,
});
AddStep(@"set room", () => inspector.Room = room);
AddStep(@"null room", () => inspector.Room = null);
AddStep(@"set room", () => inspector.Room = room);
AddStep(@"change title", () => room.Name.Value = @"A Better Room Than The Above");
AddStep(@"change host", () => room.Host.Value = new User { Username = @"DrabWeb", Id = 6946022, Country = new Country { FlagName = @"CA" } });
AddStep(@"change status", () => room.Status.Value = new RoomStatusPlaying());
@ -88,7 +92,7 @@ namespace osu.Game.Tests.Visual
AddStep(@"change room", () =>
{
var newRoom = new Room
Room newRoom = new Room
{
Name = { Value = @"My New, Better Than Ever Room" },
Host = { Value = new User { Username = @"Angelsim", Id = 1777162, Country = new Country { FlagName = @"KR" } } },

View File

@ -0,0 +1,145 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Screens;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Screens;
using OpenTK;
namespace osu.Game.Tests.Visual
{
[TestFixture]
public class TestCaseScreenBreadcrumbControl : OsuTestCase
{
private readonly ScreenBreadcrumbControl breadcrumbs;
private Screen currentScreen, changedScreen;
public TestCaseScreenBreadcrumbControl()
{
TestScreen startScreen;
OsuSpriteText titleText;
Children = new Drawable[]
{
currentScreen = startScreen = new TestScreenOne(),
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(10),
Children = new Drawable[]
{
breadcrumbs = new ScreenBreadcrumbControl(startScreen)
{
RelativeSizeAxes = Axes.X,
},
titleText = new OsuSpriteText(),
},
},
};
breadcrumbs.Current.ValueChanged += s =>
{
titleText.Text = $"Changed to {s.ToString()}";
changedScreen = s;
};
breadcrumbs.Current.TriggerChange();
assertCurrent();
pushNext();
assertCurrent();
pushNext();
assertCurrent();
AddStep(@"make start current", () =>
{
startScreen.MakeCurrent();
currentScreen = startScreen;
});
assertCurrent();
pushNext();
AddAssert(@"only 2 items", () => breadcrumbs.Items.Count() == 2);
AddStep(@"exit current", () => changedScreen.Exit());
AddAssert(@"current screen is first", () => startScreen == changedScreen);
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
breadcrumbs.StripColour = colours.Blue;
}
private void pushNext() => AddStep(@"push next screen", () => currentScreen = ((TestScreen)currentScreen).PushNext());
private void assertCurrent() => AddAssert(@"changedScreen correct", () => currentScreen == changedScreen);
private abstract class TestScreen : OsuScreen
{
protected abstract string Title { get; }
protected abstract string NextTitle { get; }
protected abstract TestScreen CreateNextScreen();
public override string ToString() => Title;
public TestScreen PushNext()
{
TestScreen screen = CreateNextScreen();
Push(screen);
return screen;
}
protected TestScreen()
{
Child = new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(10),
Children = new Drawable[]
{
new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = Title,
},
new TriangleButton
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Width = 100,
Text = $"Push {NextTitle}",
Action = () => PushNext(),
},
},
};
}
}
private class TestScreenOne : TestScreen
{
protected override string Title => @"Screen One";
protected override string NextTitle => @"Two";
protected override TestScreen CreateNextScreen() => new TestScreenTwo();
}
private class TestScreenTwo : TestScreen
{
protected override string Title => @"Screen Two";
protected override string NextTitle => @"One";
protected override TestScreen CreateNextScreen() => new TestScreenOne();
}
}
}

View File

@ -6,7 +6,6 @@ using osu.Game.Rulesets.Objects;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.IO.Serialization;
using Newtonsoft.Json;
using osu.Game.IO.Serialization.Converters;
@ -15,21 +14,27 @@ namespace osu.Game.Beatmaps
/// <summary>
/// A Beatmap containing converted HitObjects.
/// </summary>
public class Beatmap<T> : IJsonSerializable
public class Beatmap<T> : IBeatmap
where T : HitObject
{
public BeatmapInfo BeatmapInfo = new BeatmapInfo();
public ControlPointInfo ControlPointInfo = new ControlPointInfo();
public List<BreakPeriod> Breaks = new List<BreakPeriod>();
public BeatmapInfo BeatmapInfo { get; set; } = new BeatmapInfo
{
Metadata = new BeatmapMetadata
{
Artist = @"Unknown",
Title = @"Unknown",
AuthorString = @"Unknown Creator",
},
Version = @"Normal",
BaseDifficulty = new BeatmapDifficulty()
};
[JsonIgnore]
public BeatmapMetadata Metadata => BeatmapInfo?.Metadata ?? BeatmapInfo?.BeatmapSet?.Metadata;
/// <summary>
/// The HitObjects this Beatmap contains.
/// </summary>
[JsonConverter(typeof(TypedListConverter<HitObject>))]
public List<T> HitObjects = new List<T>();
public ControlPointInfo ControlPointInfo { get; set; } = new ControlPointInfo();
public List<BreakPeriod> Breaks { get; set; } = new List<BreakPeriod>();
/// <summary>
/// Total amount of break time in the beatmap.
@ -38,51 +43,22 @@ namespace osu.Game.Beatmaps
public double TotalBreakTime => Breaks.Sum(b => b.Duration);
/// <summary>
/// Constructs a new beatmap.
/// The HitObjects this Beatmap contains.
/// </summary>
/// <param name="original">The original beatmap to use the parameters of.</param>
public Beatmap(Beatmap<T> original = null)
{
BeatmapInfo = original?.BeatmapInfo.DeepClone() ?? BeatmapInfo;
ControlPointInfo = original?.ControlPointInfo ?? ControlPointInfo;
Breaks = original?.Breaks ?? Breaks;
HitObjects = original?.HitObjects ?? HitObjects;
[JsonConverter(typeof(TypedListConverter<HitObject>))]
public List<T> HitObjects = new List<T>();
if (original == null && Metadata == null)
{
// we may have no metadata in cases we weren't sourced from the database.
// let's fill it (and other related fields) so we don't need to null-check it in future usages.
BeatmapInfo = new BeatmapInfo
{
Metadata = new BeatmapMetadata
{
Artist = @"Unknown",
Title = @"Unknown",
AuthorString = @"Unknown Creator",
},
Version = @"Normal",
BaseDifficulty = new BeatmapDifficulty()
};
}
}
IEnumerable<HitObject> IBeatmap.HitObjects => HitObjects;
public virtual IEnumerable<BeatmapStatistic> GetStatistics() => Enumerable.Empty<BeatmapStatistic>();
IBeatmap IBeatmap.Clone() => Clone();
public Beatmap<T> Clone() => (Beatmap<T>)MemberwiseClone();
}
/// <summary>
/// A Beatmap containing un-converted HitObjects.
/// </summary>
public class Beatmap : Beatmap<HitObject>
{
/// <summary>
/// Constructs a new beatmap.
/// </summary>
/// <param name="original">The original beatmap to use the parameters of.</param>
public Beatmap(Beatmap original)
: base(original)
{
}
public Beatmap()
{
}
public new Beatmap Clone() => (Beatmap)base.Clone();
}
}

View File

@ -22,37 +22,37 @@ namespace osu.Game.Beatmaps
remove => ObjectConverted -= value;
}
/// <summary>
/// Checks if a Beatmap can be converted using this Beatmap Converter.
/// </summary>
/// <param name="beatmap">The Beatmap to check.</param>
/// <returns>Whether the Beatmap can be converted using this Beatmap Converter.</returns>
public bool CanConvert(Beatmap beatmap) => ValidConversionTypes.All(t => beatmap.HitObjects.Any(t.IsInstanceOfType));
public IBeatmap Beatmap { get; }
/// <summary>
/// Converts a Beatmap using this Beatmap Converter.
/// </summary>
/// <param name="original">The un-converted Beatmap.</param>
/// <returns>The converted Beatmap.</returns>
public Beatmap<T> Convert(Beatmap original)
protected BeatmapConverter(IBeatmap beatmap)
{
// We always operate on a clone of the original beatmap, to not modify it game-wide
return ConvertBeatmap(new Beatmap(original));
Beatmap = beatmap;
}
void IBeatmapConverter.Convert(Beatmap original) => Convert(original);
/// <summary>
/// Whether <see cref="Beatmap"/> can be converted by this <see cref="BeatmapConverter{T}"/>.
/// </summary>
public bool CanConvert => !Beatmap.HitObjects.Any() || ValidConversionTypes.All(t => Beatmap.HitObjects.Any(t.IsInstanceOfType));
/// <summary>
/// Converts <see cref="Beatmap"/>.
/// </summary>
/// <returns>The converted Beatmap.</returns>
public IBeatmap Convert()
{
// We always operate on a clone of the original beatmap, to not modify it game-wide
return ConvertBeatmap(Beatmap.Clone());
}
/// <summary>
/// Performs the conversion of a Beatmap using this Beatmap Converter.
/// </summary>
/// <param name="original">The un-converted Beatmap.</param>
/// <returns>The converted Beatmap.</returns>
protected virtual Beatmap<T> ConvertBeatmap(Beatmap original)
protected virtual Beatmap<T> ConvertBeatmap(IBeatmap original)
{
var beatmap = CreateBeatmap();
// todo: this *must* share logic (or directly use) Beatmap<T>'s constructor.
// right now this isn't easily possible due to generic entanglement.
beatmap.BeatmapInfo = original.BeatmapInfo;
beatmap.ControlPointInfo = original.ControlPointInfo;
beatmap.HitObjects = original.HitObjects.SelectMany(h => convert(h, original)).ToList();
@ -67,7 +67,7 @@ namespace osu.Game.Beatmaps
/// <param name="original">The hit object to convert.</param>
/// <param name="beatmap">The un-converted Beatmap.</param>
/// <returns>The converted hit object.</returns>
private IEnumerable<T> convert(HitObject original, Beatmap beatmap)
private IEnumerable<T> convert(HitObject original, IBeatmap beatmap)
{
// Check if the hitobject is already the converted type
T tObject = original as T;
@ -107,6 +107,6 @@ namespace osu.Game.Beatmaps
/// <param name="original">The hit object to convert.</param>
/// <param name="beatmap">The un-converted Beatmap.</param>
/// <returns>The converted hit object.</returns>
protected abstract IEnumerable<T> ConvertHitObject(HitObject original, Beatmap beatmap);
protected abstract IEnumerable<T> ConvertHitObject(HitObject original, IBeatmap beatmap);
}
}

View File

@ -32,6 +32,11 @@ namespace osu.Game.Beatmaps
public double SliderMultiplier { get; set; } = 1;
public double SliderTickRate { get; set; } = 1;
/// <summary>
/// Returns a shallow-clone of this <see cref="BeatmapDifficulty"/>.
/// </summary>
public BeatmapDifficulty Clone() => (BeatmapDifficulty)MemberwiseClone();
/// <summary>
/// Maps a difficulty value [0, 10] to a two-piece linear range of values.
/// </summary>

View File

@ -143,5 +143,10 @@ namespace osu.Game.Beatmaps
public bool BackgroundEquals(BeatmapInfo other) => other != null && BeatmapSet != null && other.BeatmapSet != null &&
BeatmapSet.Hash == other.BeatmapSet.Hash &&
(Metadata ?? BeatmapSet.Metadata).BackgroundFile == (other.Metadata ?? other.BeatmapSet.Metadata).BackgroundFile;
/// <summary>
/// Returns a shallow-clone of this <see cref="BeatmapInfo"/>.
/// </summary>
public BeatmapInfo Clone() => (BeatmapInfo)MemberwiseClone();
}
}

View File

@ -9,7 +9,9 @@ using System.Linq.Expressions;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Extensions;
using osu.Framework.Graphics.Textures;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Beatmaps.Formats;
@ -333,7 +335,7 @@ namespace osu.Game.Beatmaps
ms.Position = 0;
var decoder = Decoder.GetDecoder<Beatmap>(sr);
Beatmap beatmap = decoder.Decode(sr);
IBeatmap beatmap = decoder.Decode(sr);
beatmap.BeatmapInfo.Path = name;
beatmap.BeatmapInfo.Hash = ms.ComputeSHA2Hash();
@ -341,9 +343,16 @@ namespace osu.Game.Beatmaps
RulesetInfo ruleset = rulesets.GetRuleset(beatmap.BeatmapInfo.RulesetID);
// TODO: this should be done in a better place once we actually need to dynamically update it.
beatmap.BeatmapInfo.Ruleset = ruleset;
beatmap.BeatmapInfo.StarDifficulty = ruleset?.CreateInstance()?.CreateDifficultyCalculator(beatmap).Calculate() ?? 0;
if (ruleset != null)
{
// TODO: this should be done in a better place once we actually need to dynamically update it.
var converted = new DummyConversionBeatmap(beatmap).GetPlayableBeatmap(ruleset);
beatmap.BeatmapInfo.StarDifficulty = ruleset.CreateInstance().CreateDifficultyCalculator(converted).Calculate();
}
else
beatmap.BeatmapInfo.StarDifficulty = 0;
beatmapInfos.Add(beatmap.BeatmapInfo);
}
@ -351,5 +360,23 @@ namespace osu.Game.Beatmaps
return beatmapInfos;
}
/// <summary>
/// A dummy WorkingBeatmap for the purpose of retrieving a beatmap for star difficulty calculation.
/// </summary>
private class DummyConversionBeatmap : WorkingBeatmap
{
private readonly IBeatmap beatmap;
public DummyConversionBeatmap(IBeatmap beatmap)
: base(beatmap.BeatmapInfo)
{
this.beatmap = beatmap;
}
protected override IBeatmap GetBeatmap() => beatmap;
protected override Texture GetBackground() => null;
protected override Track GetTrack() => null;
}
}
}

View File

@ -30,7 +30,7 @@ namespace osu.Game.Beatmaps
this.audioManager = audioManager;
}
protected override Beatmap GetBeatmap()
protected override IBeatmap GetBeatmap()
{
try
{

View File

@ -2,30 +2,47 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Linq;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Beatmaps
{
public interface IBeatmapProcessor
{
IBeatmap Beatmap { get; }
/// <summary>
/// Post-processes <see cref="Beatmap"/> to add mode-specific components that aren't added during conversion.
/// <para>
/// An example of such a usage is for combo colours.
/// </para>
/// </summary>
void PostProcess();
}
/// <summary>
/// Processes a post-converted Beatmap.
/// </summary>
/// <typeparam name="TObject">The type of HitObject contained in the Beatmap.</typeparam>
public class BeatmapProcessor<TObject>
where TObject : HitObject
public class BeatmapProcessor : IBeatmapProcessor
{
public IBeatmap Beatmap { get; }
public BeatmapProcessor(IBeatmap beatmap)
{
Beatmap = beatmap;
}
/// <summary>
/// Post-processes a Beatmap to add mode-specific components that aren't added during conversion.
/// <para>
/// An example of such a usage is for combo colours.
/// </para>
/// </summary>
/// <param name="beatmap">The Beatmap to process.</param>
public virtual void PostProcess(Beatmap<TObject> beatmap)
public virtual void PostProcess()
{
IHasComboInformation lastObj = null;
foreach (var obj in beatmap.HitObjects.OfType<IHasComboInformation>())
foreach (var obj in Beatmap.HitObjects.OfType<IHasComboInformation>())
{
if (obj.NewCombo)
{

View File

@ -1,64 +0,0 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Rulesets.Objects;
using System.Collections.Generic;
using osu.Game.Rulesets.Mods;
using osu.Framework.Timing;
using System.Linq;
using osu.Framework.Extensions.IEnumerableExtensions;
namespace osu.Game.Beatmaps
{
public abstract class DifficultyCalculator
{
protected double TimeRate = 1;
public abstract double Calculate(Dictionary<string, double> categoryDifficulty = null);
}
public abstract class DifficultyCalculator<T> : DifficultyCalculator where T : HitObject
{
protected readonly Beatmap<T> Beatmap;
protected readonly Mod[] Mods;
protected DifficultyCalculator(Beatmap beatmap, Mod[] mods = null)
{
Mods = mods ?? new Mod[0];
var converter = CreateBeatmapConverter(beatmap);
foreach (var mod in Mods.OfType<IApplicableToBeatmapConverter<T>>())
mod.ApplyToBeatmapConverter(converter);
Beatmap = converter.Convert(beatmap);
ApplyMods(Mods);
PreprocessHitObjects();
}
protected virtual void ApplyMods(Mod[] mods)
{
var clock = new StopwatchClock();
mods.OfType<IApplicableToClock>().ForEach(m => m.ApplyToClock(clock));
TimeRate = clock.Rate;
foreach (var mod in Mods.OfType<IApplicableToDifficulty>())
mod.ApplyToDifficulty(Beatmap.BeatmapInfo.BaseDifficulty);
foreach (var h in Beatmap.HitObjects)
h.ApplyDefaults(Beatmap.ControlPointInfo, Beatmap.BeatmapInfo.BaseDifficulty);
foreach (var mod in mods.OfType<IApplicableToHitObject<T>>())
foreach (var obj in Beatmap.HitObjects)
mod.ApplyToHitObject(obj);
}
protected virtual void PreprocessHitObjects()
{
}
protected abstract BeatmapConverter<T> CreateBeatmapConverter(Beatmap beatmap);
}
}

View File

@ -6,7 +6,9 @@ using System.Collections.Generic;
using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Textures;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI;
namespace osu.Game.Beatmaps
@ -39,7 +41,7 @@ namespace osu.Game.Beatmaps
this.game = game;
}
protected override Beatmap GetBeatmap() => new Beatmap();
protected override IBeatmap GetBeatmap() => new Beatmap();
protected override Texture GetBackground() => game.Textures.Get(@"Backgrounds/bg4");
@ -53,12 +55,14 @@ namespace osu.Game.Beatmaps
{
public override IEnumerable<Mod> GetModsFor(ModType type) => new Mod[] { };
public override RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap, bool isForCurrentRuleset)
public override RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap)
{
throw new NotImplementedException();
}
public override DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap, Mod[] mods = null) => null;
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new DummyBeatmapConverter { Beatmap = beatmap };
public override DifficultyCalculator CreateDifficultyCalculator(IBeatmap beatmap, Mod[] mods = null) => null;
public override string Description => "dummy";
@ -68,6 +72,14 @@ namespace osu.Game.Beatmaps
: base(rulesetInfo)
{
}
private class DummyBeatmapConverter : IBeatmapConverter
{
public event Action<HitObject, IEnumerable<HitObject>> ObjectConverted;
public IBeatmap Beatmap { get; set; }
public bool CanConvert => true;
public IBeatmap Convert() => Beatmap;
}
}
}
}

View File

@ -54,9 +54,11 @@ namespace osu.Game.Beatmaps.Formats
base.ParseStreamInto(stream, beatmap);
// objects may be out of order *only* if a user has manually edited an .osu file.
// unfortunately there are ranked maps in this state (example: https://osu.ppy.sh/s/594828).
this.beatmap.HitObjects.Sort((x, y) => x.StartTime.CompareTo(y.StartTime));
// Objects may be out of order *only* if a user has manually edited an .osu file.
// Unfortunately there are ranked maps in this state (example: https://osu.ppy.sh/s/594828).
// OrderBy is used to guarantee that the parsing order of hitobjects with equal start times is maintained (stably-sorted)
// The parsing order of hitobjects matters in mania difficulty calculation
this.beatmap.HitObjects = this.beatmap.HitObjects.OrderBy(h => h.StartTime).ToList();
foreach (var hitObject in this.beatmap.HitObjects)
hitObject.ApplyDefaults(this.beatmap.ControlPointInfo, this.beatmap.BeatmapInfo.BaseDifficulty);

View File

@ -0,0 +1,56 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections.Generic;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Timing;
using osu.Game.IO.Serialization;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Beatmaps
{
public interface IBeatmap : IJsonSerializable
{
/// <summary>
/// This beatmap's info.
/// </summary>
BeatmapInfo BeatmapInfo { get; set; }
/// <summary>
/// This beatmap's metadata.
/// </summary>
BeatmapMetadata Metadata { get; }
/// <summary>
/// The control points in this beatmap.
/// </summary>
ControlPointInfo ControlPointInfo { get; }
/// <summary>
/// The breaks in this beatmap.
/// </summary>
List<BreakPeriod> Breaks { get; }
/// <summary>
/// Total amount of break time in the beatmap.
/// </summary>
double TotalBreakTime { get; }
/// <summary>
/// The hitobjects contained by this beatmap.
/// </summary>
IEnumerable<HitObject> HitObjects { get; }
/// <summary>
/// Returns statistics for the <see cref="HitObjects"/> contained in this beatmap.
/// </summary>
/// <returns></returns>
IEnumerable<BeatmapStatistic> GetStatistics();
/// <summary>
/// Creates a shallow-clone of this beatmap and returns it.
/// </summary>
/// <returns>The shallow-cloned beatmap.</returns>
IBeatmap Clone();
}
}

View File

@ -16,10 +16,16 @@ namespace osu.Game.Beatmaps
/// </summary>
event Action<HitObject, IEnumerable<HitObject>> ObjectConverted;
IBeatmap Beatmap { get; }
/// <summary>
/// Converts a Beatmap using this Beatmap Converter.
/// Whether <see cref="Beatmap"/> can be converted by this <see cref="IBeatmapConverter"/>.
/// </summary>
/// <param name="beatmap">The un-converted Beatmap.</param>
void Convert(Beatmap beatmap);
bool CanConvert { get; }
/// <summary>
/// Converts <see cref="Beatmap"/>.
/// </summary>
IBeatmap Convert();
}
}

View File

@ -1,21 +0,0 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
namespace osu.Game.Beatmaps.Legacy
{
/// <summary>
/// A type of Beatmap loaded from a legacy .osu beatmap file (version &lt;=15).
/// </summary>
public class LegacyBeatmap : Beatmap
{
/// <summary>
/// Constructs a new beatmap.
/// </summary>
/// <param name="original">The original beatmap to use the parameters of.</param>
internal LegacyBeatmap(Beatmap original = null)
: base(original)
{
HitObjects = original?.HitObjects;
}
}
}

View File

@ -14,6 +14,8 @@ using osu.Framework.IO.File;
using System.IO;
using osu.Game.IO.Serialization;
using System.Diagnostics;
using osu.Game.Rulesets;
using osu.Game.Rulesets.UI;
using osu.Game.Skinning;
namespace osu.Game.Beatmaps
@ -36,7 +38,7 @@ namespace osu.Game.Beatmaps
Mods.ValueChanged += mods => applyRateAdjustments();
beatmap = new AsyncLazy<Beatmap>(populateBeatmap);
beatmap = new AsyncLazy<IBeatmap>(populateBeatmap);
background = new AsyncLazy<Texture>(populateBackground, b => b == null || !b.IsDisposed);
track = new AsyncLazy<Track>(populateTrack);
waveform = new AsyncLazy<Waveform>(populateWaveform);
@ -45,7 +47,7 @@ namespace osu.Game.Beatmaps
}
/// <summary>
/// Saves the <see cref="Beatmap"/>.
/// Saves the <see cref="Beatmaps.Beatmap"/>.
/// </summary>
public void Save()
{
@ -55,7 +57,7 @@ namespace osu.Game.Beatmaps
Process.Start(path);
}
protected abstract Beatmap GetBeatmap();
protected abstract IBeatmap GetBeatmap();
protected abstract Texture GetBackground();
protected abstract Track GetTrack();
protected virtual Skin GetSkin() => new DefaultSkin();
@ -63,12 +65,11 @@ namespace osu.Game.Beatmaps
protected virtual Storyboard GetStoryboard() => new Storyboard { BeatmapInfo = BeatmapInfo };
public bool BeatmapLoaded => beatmap.IsResultAvailable;
public Beatmap Beatmap => beatmap.Value.Result;
public async Task<Beatmap> GetBeatmapAsync() => await beatmap.Value;
public IBeatmap Beatmap => beatmap.Value.Result;
public async Task<IBeatmap> GetBeatmapAsync() => await beatmap.Value;
private readonly AsyncLazy<IBeatmap> beatmap;
private readonly AsyncLazy<Beatmap> beatmap;
private Beatmap populateBeatmap()
private IBeatmap populateBeatmap()
{
var b = GetBeatmap() ?? new Beatmap();
@ -78,6 +79,57 @@ namespace osu.Game.Beatmaps
return b;
}
/// <summary>
/// Constructs a playable <see cref="IBeatmap"/> from <see cref="Beatmap"/> using the applicable converters for a specific <see cref="RulesetInfo"/>.
/// <para>
/// The returned <see cref="IBeatmap"/> is in a playable state - all <see cref="HitObject"/> and <see cref="BeatmapDifficulty"/> <see cref="Mod"/>s
/// have been applied, and <see cref="HitObject"/>s have been fully constructed.
/// </para>
/// </summary>
/// <param name="ruleset">The <see cref="RulesetInfo"/> to create a playable <see cref="IBeatmap"/> for.</param>
/// <returns>The converted <see cref="IBeatmap"/>.</returns>
/// <exception cref="BeatmapInvalidForRulesetException">If <see cref="Beatmap"/> could not be converted to <paramref name="ruleset"/>.</exception>
public IBeatmap GetPlayableBeatmap(RulesetInfo ruleset)
{
var rulesetInstance = ruleset.CreateInstance();
IBeatmapConverter converter = rulesetInstance.CreateBeatmapConverter(Beatmap);
// Check if the beatmap can be converted
if (!converter.CanConvert)
throw new BeatmapInvalidForRulesetException($"{nameof(Beatmaps.Beatmap)} can not be converted for the ruleset (ruleset: {ruleset.InstantiationInfo}, converter: {converter}).");
// Apply conversion mods
foreach (var mod in Mods.Value.OfType<IApplicableToBeatmapConverter>())
mod.ApplyToBeatmapConverter(converter);
// Convert
IBeatmap converted = converter.Convert();
// Apply difficulty mods
if (Mods.Value.Any(m => m is IApplicableToDifficulty))
{
converted.BeatmapInfo = converted.BeatmapInfo.Clone();
converted.BeatmapInfo.BaseDifficulty = converted.BeatmapInfo.BaseDifficulty.Clone();
foreach (var mod in Mods.Value.OfType<IApplicableToDifficulty>())
mod.ApplyToDifficulty(converted.BeatmapInfo.BaseDifficulty);
}
// Post-process
rulesetInstance.CreateBeatmapProcessor(converted)?.PostProcess();
// Compute default values for hitobjects, including creating nested hitobjects in-case they're needed
foreach (var obj in converted.HitObjects)
obj.ApplyDefaults(converted.ControlPointInfo, converted.BeatmapInfo.BaseDifficulty);
foreach (var mod in Mods.Value.OfType<IApplicableToHitObject>())
foreach (var obj in converted.HitObjects)
mod.ApplyToHitObject(obj);
return converted;
}
public bool BackgroundLoaded => background.IsResultAvailable;
public Texture Background => background.Value.Result;
public async Task<Texture> GetBackgroundAsync() => await background.Value;

View File

@ -2,6 +2,7 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Configuration;
using osu.Framework.Configuration.Tracking;
using osu.Framework.Platform;
using osu.Game.Overlays;
using osu.Game.Screens.Select;
@ -95,6 +96,11 @@ namespace osu.Game.Configuration
public OsuConfigManager(Storage storage) : base(storage)
{
}
public override TrackedSettings CreateTrackedSettings() => new TrackedSettings
{
new TrackedSetting<bool>(OsuSetting.MouseDisableButtons, v => new SettingDescription(!v, "gameplay mouse buttons", v ? "disabled" : "enabled"))
};
}
public enum OsuSetting

View File

@ -44,19 +44,6 @@ namespace osu.Game.Graphics.Containers
return base.OnClick(state);
}
protected override bool OnDragStart(InputState state)
{
if (!base.ReceiveMouseInputAt(state.Mouse.NativeState.Position))
{
State = Visibility.Hidden;
return true;
}
return base.OnDragStart(state);
}
protected override bool OnDrag(InputState state) => State == Visibility.Hidden;
private void onStateChanged(Visibility visibility)
{
switch (visibility)

Some files were not shown because too many files have changed in this diff Show More