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

Merge branch 'master' into fix-background-screen-crash

This commit is contained in:
Dean Herbert 2018-06-27 16:00:59 +09:00 committed by GitHub
commit 527d7c29f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
140 changed files with 5994 additions and 2332 deletions

8
.github/pull_request_template.md vendored Normal file
View File

@ -0,0 +1,8 @@
Add any details pertaining to developers above the break.
- [ ] Depends on #PR
- Closes #ISSUE
---
Add a sentence or two describing this change in plain english. This will be displayed on the [changelog](https://osu.ppy.sh/home/changelog). A single screenshot or short gif is also welcomed.

View File

@ -1,29 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [{
"name": "Deploy (Debug)",
"request": "launch",
"type": "mono",
"program": "${workspaceRoot}/bin/Debug/net471/osu.Desktop.Deploy.exe",
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Debug)",
"runtimeExecutable": null,
"env": {},
"console": "internalConsole"
},
{
"name": "Deploy (Release)",
"request": "launch",
"type": "clr",
"program": "${workspaceRoot}/bin/Release/net471/osu.Desktop.Deploy.exe",
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Release)",
"runtimeExecutable": null,
"env": {},
"console": "internalConsole"
}
]
}

View File

@ -1,64 +0,0 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"command": "msbuild",
"type": "shell",
"suppressTaskName": true,
"args": [
"/property:GenerateFullPaths=true",
"/property:DebugType=portable",
"/verbosity:minimal",
"/m" //parallel compiling support.
],
"tasks": [{
"taskName": "Build (Debug)",
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": [
"$msCompile"
]
},
{
"taskName": "Build (Release)",
"group": "build",
"args": [
"/property:Configuration=Release"
],
"problemMatcher": [
"$msCompile"
]
},
{
"taskName": "Clean (Debug)",
"args": [
"/target:Clean"
],
"problemMatcher": [
"$msCompile"
]
},
{
"taskName": "Clean (Release)",
"args": [
"/target:Clean",
"/property:Configuration=Release"
],
"problemMatcher": [
"$msCompile"
]
},
{
"taskName": "Clean All",
"dependsOn": [
"Clean (Debug)",
"Clean (Release)"
],
"problemMatcher": [
"$msCompile"
]
}
]
}

View File

@ -73,7 +73,7 @@ namespace osu.Desktop
}
public StableStorage()
: base(string.Empty)
: base(string.Empty, null)
{
}
}

View File

@ -29,7 +29,8 @@
<PackageReference Include="Microsoft.Win32.Registry" Version="4.5.0" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.1.1" />
<PackageReference Include="squirrel.windows" Version="1.8.0" Condition="'$(TargetFramework)' == 'net471'" />
</ItemGroup>
<ItemGroup Label="Resources">

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using NUnit.Framework;
using osu.Framework.MathUtils;
using osu.Game.Rulesets.Catch.Objects;
@ -12,11 +13,14 @@ using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Catch.Tests
{
internal class CatchBeatmapConversionTest : BeatmapConversionTest<ConvertValue>
[TestFixture]
public class CatchBeatmapConversionTest : BeatmapConversionTest<ConvertValue>
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Catch";
[TestCase("basic"), Ignore("See: https://github.com/ppy/osu/issues/2232")]
[TestCase("basic")]
[TestCase("spinner")]
[TestCase("spinner-and-circles")]
public new void Test(string name)
{
base.Test(name);
@ -27,36 +31,52 @@ namespace osu.Game.Rulesets.Catch.Tests
if (hitObject is JuiceStream stream)
{
foreach (var nested in stream.NestedHitObjects)
{
yield return new ConvertValue
{
StartTime = nested.StartTime,
Position = ((CatchHitObject)nested).X * CatchPlayfield.BASE_WIDTH
};
}
yield return new ConvertValue((CatchHitObject)nested);
}
else if (hitObject is BananaShower shower)
{
foreach (var nested in shower.NestedHitObjects)
yield return new ConvertValue((CatchHitObject)nested);
}
else
{
yield return new ConvertValue
{
StartTime = hitObject.StartTime,
Position = ((CatchHitObject)hitObject).X * CatchPlayfield.BASE_WIDTH
};
}
yield return new ConvertValue((CatchHitObject)hitObject);
}
protected override Ruleset CreateRuleset() => new CatchRuleset();
}
internal struct ConvertValue : IEquatable<ConvertValue>
public struct ConvertValue : IEquatable<ConvertValue>
{
/// <summary>
/// A sane value to account for osu!stable using ints everwhere.
/// </summary>
private const float conversion_lenience = 2;
public double StartTime;
public float Position;
[JsonIgnore]
public readonly CatchHitObject HitObject;
public ConvertValue(CatchHitObject hitObject)
{
HitObject = hitObject;
startTime = 0;
position = 0;
}
private double startTime;
public double StartTime
{
get => HitObject?.StartTime ?? startTime;
set => startTime = value;
}
private float position;
public float Position
{
get => HitObject?.X * CatchPlayfield.BASE_WIDTH ?? position;
set => position = value;
}
public bool Equals(ConvertValue other)
=> Precision.AlmostEquals(StartTime, other.StartTime, conversion_lenience)

View File

@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Catch.Tests
{
}
public void ToggleHyperDash(bool status) => MovableCatcher.HyperDashModifier = status ? 2 : 1;
public void ToggleHyperDash(bool status) => MovableCatcher.SetHyperdashState(status ? 2 : 1);
}
}
}

View File

@ -26,22 +26,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
var positionData = obj as IHasXPosition;
var comboData = obj as IHasCombo;
var endTime = obj as IHasEndTime;
if (positionData == null)
{
if (endTime != null)
{
yield return new BananaShower
{
StartTime = obj.StartTime,
Samples = obj.Samples,
Duration = endTime.Duration,
NewCombo = comboData?.NewCombo ?? false
};
}
yield break;
}
var legacyOffset = obj as IHasLegacyLastTickOffset;
if (curveData != null)
{
@ -54,20 +39,31 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
Distance = curveData.Distance,
RepeatSamples = curveData.RepeatSamples,
RepeatCount = curveData.RepeatCount,
X = positionData.X / CatchPlayfield.BASE_WIDTH,
X = (positionData?.X ?? 0) / CatchPlayfield.BASE_WIDTH,
NewCombo = comboData?.NewCombo ?? false,
LegacyLastTickOffset = legacyOffset?.LegacyLastTickOffset ?? 0
};
}
else if (endTime != null)
{
yield return new BananaShower
{
StartTime = obj.StartTime,
Samples = obj.Samples,
Duration = endTime.Duration,
NewCombo = comboData?.NewCombo ?? false
};
yield break;
}
yield return new Fruit
else
{
StartTime = obj.StartTime,
Samples = obj.Samples,
NewCombo = comboData?.NewCombo ?? false,
X = positionData.X / CatchPlayfield.BASE_WIDTH
};
yield return new Fruit
{
StartTime = obj.StartTime,
Samples = obj.Samples,
NewCombo = comboData?.NewCombo ?? false,
X = (positionData?.X ?? 0) / CatchPlayfield.BASE_WIDTH
};
}
}
protected override Beatmap<CatchHitObject> CreateBeatmap() => new CatchBeatmap();

View File

@ -9,6 +9,7 @@ using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects.Types;
using OpenTK;
using osu.Game.Rulesets.Catch.MathUtils;
namespace osu.Game.Rulesets.Catch.Beatmaps
{
@ -21,13 +22,54 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
public override void PostProcess()
{
applyPositionOffsets();
initialiseHyperDash((List<CatchHitObject>)Beatmap.HitObjects);
base.PostProcess();
int index = 0;
foreach (var obj in Beatmap.HitObjects.OfType<CatchHitObject>())
{
obj.IndexInBeatmap = index++;
if (obj.LastInCombo && obj.NestedHitObjects.LastOrDefault() is IHasComboInformation lastNested)
lastNested.LastInCombo = true;
}
}
public const int RNG_SEED = 1337;
private void applyPositionOffsets()
{
var rng = new FastRandom(RNG_SEED);
// todo: HardRock displacement should be applied here
foreach (var obj in Beatmap.HitObjects)
{
switch (obj)
{
case BananaShower bananaShower:
foreach (var nested in bananaShower.NestedHitObjects)
{
((BananaShower.Banana)nested).X = (float)rng.NextDouble();
rng.Next(); // osu!stable retrieved a random banana type
rng.Next(); // osu!stable retrieved a random banana rotation
rng.Next(); // osu!stable retrieved a random banana colour
}
break;
case JuiceStream juiceStream:
foreach (var nested in juiceStream.NestedHitObjects)
{
var hitObject = (CatchHitObject)nested;
if (hitObject is TinyDroplet)
hitObject.X += rng.Next(-20, 20) / CatchPlayfield.BASE_WIDTH;
else if (hitObject is Droplet)
rng.Next(); // osu!stable retrieved a random droplet rotation
hitObject.X = MathHelper.Clamp(hitObject.X, 0, 1);
}
break;
}
}
}
private void initialiseHyperDash(List<CatchHitObject> objects)

View File

@ -0,0 +1,19 @@
// 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.Difficulty;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Catch.Difficulty
{
public class CatchDifficultyAttributes : DifficultyAttributes
{
public double ApproachRate;
public int MaxCombo;
public CatchDifficultyAttributes(Mod[] mods, double starRating)
: base(mods, starRating)
{
}
}
}

View File

@ -1,19 +1,146 @@
// 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.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
namespace osu.Game.Rulesets.Catch.Difficulty
{
public class CatchDifficultyCalculator : DifficultyCalculator
{
/// <summary>
/// In milliseconds. For difficulty calculation we will only look at the highest strain value in each time interval of size STRAIN_STEP.
/// This is to eliminate higher influence of stream over aim by simply having more HitObjects with high strain.
/// The higher this value, the less strains there will be, indirectly giving long beatmaps an advantage.
/// </summary>
private const double strain_step = 750;
/// <summary>
/// The weighting of each strain value decays to this number * it's previous value
/// </summary>
private const double decay_weight = 0.94;
private const double star_scaling_factor = 0.145;
public CatchDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap)
{
}
protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double timeRate) => new DifficultyAttributes(mods, 0);
protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double timeRate)
{
if (!beatmap.HitObjects.Any())
return new CatchDifficultyAttributes(mods, 0);
var catcher = new CatcherArea.Catcher(beatmap.BeatmapInfo.BaseDifficulty);
float halfCatchWidth = catcher.CatchWidth * 0.5f;
var difficultyHitObjects = new List<CatchDifficultyHitObject>();
foreach (var hitObject in beatmap.HitObjects)
{
// We want to only consider fruits that contribute to the combo. Droplets are addressed as accuracy and spinners are not relevant for "skill" calculations.
if (hitObject is Fruit)
{
difficultyHitObjects.Add(new CatchDifficultyHitObject((CatchHitObject)hitObject, halfCatchWidth));
}
if (hitObject is JuiceStream)
difficultyHitObjects.AddRange(hitObject.NestedHitObjects.OfType<CatchHitObject>().Where(o => !(o is TinyDroplet)).Select(o => new CatchDifficultyHitObject(o, halfCatchWidth)));
}
difficultyHitObjects.Sort((a, b) => a.BaseHitObject.StartTime.CompareTo(b.BaseHitObject.StartTime));
if (!calculateStrainValues(difficultyHitObjects, timeRate))
return new CatchDifficultyAttributes(mods, 0);
// this is the same as osu!, so there's potential to share the implementation... maybe
double preEmpt = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / timeRate;
double starRating = Math.Sqrt(calculateDifficulty(difficultyHitObjects, timeRate)) * star_scaling_factor;
return new CatchDifficultyAttributes(mods, starRating)
{
ApproachRate = preEmpt > 1200.0 ? -(preEmpt - 1800.0) / 120.0 : -(preEmpt - 1200.0) / 150.0 + 5.0,
MaxCombo = difficultyHitObjects.Count
};
}
private bool calculateStrainValues(List<CatchDifficultyHitObject> objects, double timeRate)
{
CatchDifficultyHitObject lastObject = null;
if (!objects.Any()) return false;
// Traverse hitObjects in pairs to calculate the strain value of NextHitObject from the strain value of CurrentHitObject and environment.
foreach (var currentObject in objects)
{
if (lastObject != null)
currentObject.CalculateStrains(lastObject, timeRate);
lastObject = currentObject;
}
return true;
}
private double calculateDifficulty(List<CatchDifficultyHitObject> objects, double timeRate)
{
// The strain step needs to be adjusted for the algorithm to be considered equal with speed changing mods
double actualStrainStep = strain_step * timeRate;
// Find the highest strain value within each strain step
var highestStrains = new List<double>();
double intervalEndTime = actualStrainStep;
double maximumStrain = 0; // We need to keep track of the maximum strain in the current interval
CatchDifficultyHitObject previousHitObject = null;
foreach (CatchDifficultyHitObject hitObject in objects)
{
// While we are beyond the current interval push the currently available maximum to our strain list
while (hitObject.BaseHitObject.StartTime > intervalEndTime)
{
highestStrains.Add(maximumStrain);
// The maximum strain of the next interval is not zero by default! We need to take the last hitObject we encountered, take its strain and apply the decay
// until the beginning of the next interval.
if (previousHitObject == null)
{
maximumStrain = 0;
}
else
{
double decay = Math.Pow(CatchDifficultyHitObject.DECAY_BASE, (intervalEndTime - previousHitObject.BaseHitObject.StartTime) / 1000);
maximumStrain = previousHitObject.Strain * decay;
}
// Go to the next time interval
intervalEndTime += actualStrainStep;
}
// Obtain maximum strain
maximumStrain = Math.Max(hitObject.Strain, maximumStrain);
previousHitObject = hitObject;
}
// Build the weighted sum over the highest strains for each interval
double difficulty = 0;
double weight = 1;
highestStrains.Sort((a, b) => b.CompareTo(a)); // Sort from highest to lowest strain.
foreach (double strain in highestStrains)
{
difficulty += weight * strain;
weight *= decay_weight;
}
return difficulty;
}
}
}

View File

@ -0,0 +1,130 @@
// 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 osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using OpenTK;
namespace osu.Game.Rulesets.Catch.Difficulty
{
public class CatchDifficultyHitObject
{
internal static readonly double DECAY_BASE = 0.20;
private const float normalized_hitobject_radius = 41.0f;
private const float absolute_player_positioning_error = 16f;
private readonly float playerPositioningError;
internal CatchHitObject BaseHitObject;
/// <summary>
/// Measures jump difficulty. CtB doesn't have something like button pressing speed or accuracy
/// </summary>
internal double Strain = 1;
/// <summary>
/// This is required to keep track of lazy player movement (always moving only as far as necessary)
/// Without this quick repeat sliders / weirdly shaped streams might become ridiculously overrated
/// </summary>
internal float PlayerPositionOffset;
internal float LastMovement;
internal float NormalizedPosition;
internal float ActualNormalizedPosition => NormalizedPosition + PlayerPositionOffset;
internal CatchDifficultyHitObject(CatchHitObject baseHitObject, float catcherWidthHalf)
{
BaseHitObject = baseHitObject;
// We will scale everything by this factor, so we can assume a uniform CircleSize among beatmaps.
float scalingFactor = normalized_hitobject_radius / catcherWidthHalf;
playerPositioningError = absolute_player_positioning_error; // * scalingFactor;
NormalizedPosition = baseHitObject.X * CatchPlayfield.BASE_WIDTH * scalingFactor;
}
private const double direction_change_bonus = 12.5;
internal void CalculateStrains(CatchDifficultyHitObject previousHitObject, double timeRate)
{
// Rather simple, but more specialized things are inherently inaccurate due to the big difference playstyles and opinions make.
// See Taiko feedback thread.
double timeElapsed = (BaseHitObject.StartTime - previousHitObject.BaseHitObject.StartTime) / timeRate;
double decay = Math.Pow(DECAY_BASE, timeElapsed / 1000);
// Update new position with lazy movement.
PlayerPositionOffset =
MathHelper.Clamp(
previousHitObject.ActualNormalizedPosition,
NormalizedPosition - (normalized_hitobject_radius - playerPositioningError),
NormalizedPosition + (normalized_hitobject_radius - playerPositioningError)) // Obtain new lazy position, but be stricter by allowing for an error of a certain degree of the player.
- NormalizedPosition; // Subtract HitObject position to obtain offset
LastMovement = DistanceTo(previousHitObject);
double addition = spacingWeight(LastMovement);
if (NormalizedPosition < previousHitObject.NormalizedPosition)
{
LastMovement = -LastMovement;
}
CatchHitObject previousHitCircle = previousHitObject.BaseHitObject;
double additionBonus = 0;
double sqrtTime = Math.Sqrt(Math.Max(timeElapsed, 25));
// Direction changes give an extra point!
if (Math.Abs(LastMovement) > 0.1)
{
if (Math.Abs(previousHitObject.LastMovement) > 0.1 && Math.Sign(LastMovement) != Math.Sign(previousHitObject.LastMovement))
{
double bonus = direction_change_bonus / sqrtTime;
// Weight bonus by how
double bonusFactor = Math.Min(playerPositioningError, Math.Abs(LastMovement)) / playerPositioningError;
// We want time to play a role twice here!
addition += bonus * bonusFactor;
// Bonus for tougher direction switches and "almost" hyperdashes at this point
if (previousHitCircle != null && previousHitCircle.DistanceToHyperDash <= 10.0f / CatchPlayfield.BASE_WIDTH)
{
additionBonus += 0.3 * bonusFactor;
}
}
// Base bonus for every movement, giving some weight to streams.
addition += 7.5 * Math.Min(Math.Abs(LastMovement), normalized_hitobject_radius * 2) / (normalized_hitobject_radius * 6) / sqrtTime;
}
// Bonus for "almost" hyperdashes at corner points
if (previousHitCircle != null && previousHitCircle.DistanceToHyperDash <= 10.0f / CatchPlayfield.BASE_WIDTH)
{
if (!previousHitCircle.HyperDash)
{
additionBonus += 1.0;
}
else
{
// After a hyperdash we ARE in the correct position. Always!
PlayerPositionOffset = 0;
}
addition *= 1.0 + additionBonus * ((10 - previousHitCircle.DistanceToHyperDash * CatchPlayfield.BASE_WIDTH) / 10);
}
addition *= 850.0 / Math.Max(timeElapsed, 25);
Strain = previousHitObject.Strain * decay + addition;
}
private static double spacingWeight(float distance)
{
return Math.Pow(distance, 1.3) / 500;
}
internal float DistanceTo(CatchDifficultyHitObject other)
{
return Math.Abs(ActualNormalizedPosition - other.ActualNormalizedPosition);
}
}
}

View File

@ -0,0 +1,91 @@
// 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;
namespace osu.Game.Rulesets.Catch.MathUtils
{
/// <summary>
/// A PRNG specified in http://heliosphan.org/fastrandom.html.
/// </summary>
public class FastRandom
{
private const double int_to_real = 1.0 / (int.MaxValue + 1.0);
private const uint int_mask = 0x7FFFFFFF;
private const uint y = 842502087;
private const uint z = 3579807591;
private const uint w = 273326509;
private uint _x, _y = y, _z = z, _w = w;
public FastRandom(int seed)
{
_x = (uint)seed;
}
public FastRandom()
: this(Environment.TickCount)
{
}
/// <summary>
/// Generates a random unsigned integer within the range [<see cref="uint.MinValue"/>, <see cref="uint.MaxValue"/>).
/// </summary>
/// <returns>The random value.</returns>
public uint NextUInt()
{
uint t = _x ^ _x << 11;
_x = _y;
_y = _z;
_z = _w;
return _w = _w ^ _w >> 19 ^ t ^ t >> 8;
}
/// <summary>
/// Generates a random integer value within the range [0, <see cref="int.MaxValue"/>).
/// </summary>
/// <returns>The random value.</returns>
public int Next() => (int)(int_mask & NextUInt());
/// <summary>
/// Generates a random integer value within the range [0, <paramref name="upperBound"/>).
/// </summary>
/// <param name="upperBound">The upper bound.</param>
/// <returns>The random value.</returns>
public int Next(int upperBound) => (int)(NextDouble() * upperBound);
/// <summary>
/// Generates a random integer value within the range [<paramref name="lowerBound"/>, <paramref name="upperBound"/>).
/// </summary>
/// <param name="lowerBound">The lower bound of the range.</param>
/// <param name="upperBound">The upper bound of the range.</param>
/// <returns>The random value.</returns>
public int Next(int lowerBound, int upperBound) => (int)(lowerBound + NextDouble() * (upperBound - lowerBound));
/// <summary>
/// Generates a random double value within the range [0, 1).
/// </summary>
/// <returns>The random value.</returns>
public double NextDouble() => int_to_real * Next();
private uint bitBuffer;
private int bitIndex = 32;
/// <summary>
/// Generates a reandom boolean value. Cached such that a random value is only generated once in every 32 calls.
/// </summary>
/// <returns>The random value.</returns>
public bool NextBool()
{
if (bitIndex == 32)
{
bitBuffer = NextUInt();
bitIndex = 1;
return (bitBuffer & 1) == 1;
}
bitIndex++;
return ((bitBuffer >>= 1) & 1) == 1;
}
}
}

View File

@ -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.MathUtils;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Catch.Objects
@ -31,8 +30,7 @@ namespace osu.Game.Rulesets.Catch.Objects
AddNested(new Banana
{
Samples = Samples,
StartTime = i,
X = RNG.NextSingle()
StartTime = i
});
}

View File

@ -24,6 +24,11 @@ namespace osu.Game.Rulesets.Catch.Objects
public int ComboIndex { get; set; }
/// <summary>
/// The distance for a fruit to to next hyper if it's not a hyper.
/// </summary>
public float DistanceToHyperDash { get; set; }
/// <summary>
/// The next fruit starts a new combo. Used for explodey.
/// </summary>

View File

@ -42,7 +42,6 @@ namespace osu.Game.Rulesets.Catch.Objects
protected override void CreateNestedHitObjects()
{
base.CreateNestedHitObjects();
createTicks();
}
@ -78,6 +77,13 @@ namespace osu.Game.Rulesets.Catch.Objects
double time = spanStartTime + timeProgress * spanDuration;
if (LegacyLastTickOffset != null)
{
// If we're the last tick, apply the legacy offset
if (span == this.SpanCount() - 1 && d + tickDistance > length)
time = Math.Max(StartTime + Duration / 2, time - LegacyLastTickOffset.Value);
}
double tinyTickInterval = time - lastDropletTime;
while (tinyTickInterval > 100)
tinyTickInterval /= 2;
@ -124,9 +130,6 @@ namespace osu.Game.Rulesets.Catch.Objects
X = X + Curve.PositionAt(reversed ? 0 : 1).X / CatchPlayfield.BASE_WIDTH
});
}
if (NestedHitObjects.LastOrDefault() is IHasComboInformation lastNested)
lastNested.LastInCombo = LastInCombo;
}
public double EndTime => StartTime + this.SpanCount() * Curve.Distance / Velocity;
@ -156,5 +159,7 @@ namespace osu.Game.Rulesets.Catch.Objects
get { return Curve.CurveType; }
set { Curve.CurveType = value; }
}
public double? LegacyLastTickOffset { get; set; }
}
}

View File

@ -28,9 +28,9 @@ namespace osu.Game.Rulesets.Catch.Replays
}
}
public override List<InputState> GetPendingStates()
public override List<IInput> GetPendingInputs()
{
if (!Position.HasValue) return new List<InputState>();
if (!Position.HasValue) return new List<IInput>();
var actions = new List<CatchAction>();
@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Catch.Replays
else if (Position.Value < CurrentFrame.Position)
actions.Add(CatchAction.MoveLeft);
return new List<InputState>
return new List<IInput>
{
new CatchReplayState
{

View File

@ -0,0 +1,65 @@
{
"Mappings": [{
"StartTime": 2589,
"Objects": [{
"StartTime": 2589,
"Position": 256
}]
},
{
"StartTime": 2915,
"Objects": [{
"StartTime": 2915,
"Position": 65
},
{
"StartTime": 2916,
"Position": 482
}
]
},
{
"StartTime": 3078,
"Objects": [{
"StartTime": 3078,
"Position": 164
},
{
"StartTime": 3079,
"Position": 315
}
]
},
{
"StartTime": 3241,
"Objects": [{
"StartTime": 3241,
"Position": 145
},
{
"StartTime": 3242,
"Position": 159
}
]
},
{
"StartTime": 3404,
"Objects": [{
"StartTime": 3404,
"Position": 310
},
{
"StartTime": 3405,
"Position": 441
}
]
},
{
"StartTime": 5197,
"Objects": [{
"StartTime": 5197,
"Position": 256
}]
}
]
}

View File

@ -0,0 +1,24 @@
osu file format v14
[General]
StackLeniency: 0.7
Mode: 2
[Difficulty]
HPDrainRate:5
CircleSize:2
OverallDifficulty:5
ApproachRate:8
SliderMultiplier:1.4
SliderTickRate:4
[TimingPoints]
2589,326.086956521739,4,2,1,70,1,0
[HitObjects]
256,192,2589,5,0,0:0:0:0:
256,192,2915,12,0,2916,0:0:0:0:
256,192,3078,12,0,3079,0:0:0:0:
256,192,3241,12,0,3242,0:0:0:0:
256,192,3404,12,0,3405,0:0:0:0:
256,192,5197,5,0,0:0:0:0:

View File

@ -0,0 +1,74 @@
{
"Mappings": [{
"StartTime": 18500,
"Objects": [{
"StartTime": 18500,
"Position": 65
},
{
"StartTime": 18559,
"Position": 482
},
{
"StartTime": 18618,
"Position": 164
},
{
"StartTime": 18678,
"Position": 315
},
{
"StartTime": 18737,
"Position": 145
},
{
"StartTime": 18796,
"Position": 159
},
{
"StartTime": 18856,
"Position": 310
},
{
"StartTime": 18915,
"Position": 441
},
{
"StartTime": 18975,
"Position": 428
},
{
"StartTime": 19034,
"Position": 243
},
{
"StartTime": 19093,
"Position": 422
},
{
"StartTime": 19153,
"Position": 481
},
{
"StartTime": 19212,
"Position": 104
},
{
"StartTime": 19271,
"Position": 473
},
{
"StartTime": 19331,
"Position": 135
},
{
"StartTime": 19390,
"Position": 360
},
{
"StartTime": 19450,
"Position": 123
}
]
}]
}

View File

@ -0,0 +1,20 @@
osu file format v14
[General]
Mode: 2
[Difficulty]
HPDrainRate:6
CircleSize:4
OverallDifficulty:7
ApproachRate:8.3
SliderMultiplier:1.6
SliderTickRate:1
[TimingPoints]
500,500,4,2,1,50,1,0
13426,-100,4,3,1,45,0,0
14884,-100,4,2,1,50,0,0
[HitObjects]
256,192,18500,12,0,19450,0:0:0:0:

View File

@ -16,6 +16,7 @@ using osu.Game.Rulesets.Catch.Objects.Drawable;
using osu.Game.Rulesets.Catch.Replays;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
using OpenTK;
using OpenTK.Graphics;
@ -93,7 +94,7 @@ namespace osu.Game.Rulesets.Catch.UI
{
base.UpdateAfterChildren();
var state = GetContainingInputManager().CurrentState as CatchFramedReplayInputHandler.CatchReplayState;
var state = (GetContainingInputManager().CurrentState as RulesetInputManagerInputState<CatchAction>)?.LastReplayState as CatchFramedReplayInputHandler.CatchReplayState;
if (state?.CatcherX != null)
MovableCatcher.X = state.CatcherX.Value;
@ -105,6 +106,11 @@ namespace osu.Game.Rulesets.Catch.UI
public class Catcher : Container, IKeyBindingHandler<CatchAction>
{
/// <summary>
/// Width of the area that can be used to attempt catches during gameplay.
/// </summary>
internal float CatchWidth => CATCHER_SIZE * Math.Abs(Scale.X);
private Container<DrawableHitObject> caughtFruit;
public Container ExplodingFruitTarget;
@ -232,63 +238,74 @@ namespace osu.Game.Rulesets.Catch.UI
/// <returns>Whether the catch is possible.</returns>
public bool AttemptCatch(CatchHitObject fruit)
{
double halfCatcherWidth = CATCHER_SIZE * Math.Abs(Scale.X) * 0.5f;
float halfCatchWidth = CatchWidth * 0.5f;
// this stuff wil disappear once we move fruit to non-relative coordinate space in the future.
var catchObjectPosition = fruit.X * CatchPlayfield.BASE_WIDTH;
var catcherPosition = Position.X * CatchPlayfield.BASE_WIDTH;
var validCatch =
catchObjectPosition >= catcherPosition - halfCatcherWidth &&
catchObjectPosition <= catcherPosition + halfCatcherWidth;
catchObjectPosition >= catcherPosition - halfCatchWidth &&
catchObjectPosition <= catcherPosition + halfCatchWidth;
if (validCatch && fruit.HyperDash)
{
HyperDashModifier = Math.Abs(fruit.HyperDashTarget.X - fruit.X) / Math.Abs(fruit.HyperDashTarget.StartTime - fruit.StartTime) / BASE_SPEED;
HyperDashDirection = fruit.HyperDashTarget.X - fruit.X;
var target = fruit.HyperDashTarget;
double timeDifference = target.StartTime - fruit.StartTime;
double positionDifference = target.X * CatchPlayfield.BASE_WIDTH - catcherPosition;
double velocity = positionDifference / Math.Max(1.0, timeDifference - 1000.0 / 60.0);
SetHyperdashState(Math.Abs(velocity), target.X);
}
else
HyperDashModifier = 1;
{
SetHyperdashState();
}
return validCatch;
}
private double hyperDashModifier = 1;
private int hyperDashDirection;
private float hyperDashTargetPosition;
/// <summary>
/// Whether we are hypderdashing or not.
/// </summary>
public bool HyperDashing => hyperDashModifier != 1;
private double hyperDashModifier = 1;
/// <summary>
/// The direction in which hyperdash is allowed. 0 allows both directions.
/// Set hyperdash state.
/// </summary>
public double HyperDashDirection;
/// <summary>
/// The speed modifier resultant from hyperdash. Will trigger hyperdash when not equal to 1.
/// </summary>
public double HyperDashModifier
/// <param name="modifier">The speed multiplier. If this is less or equals to 1, this catcher will be non-hyperdashing state.</param>
/// <param name="targetPosition">When this catcher crosses this position, this catcher ends hyperdashing.</param>
public void SetHyperdashState(double modifier = 1, float targetPosition = -1)
{
get { return hyperDashModifier; }
set
const float hyperdash_transition_length = 180;
bool previouslyHyperDashing = HyperDashing;
if (modifier <= 1 || X == targetPosition)
{
if (value == hyperDashModifier) return;
hyperDashModifier = value;
hyperDashModifier = 1;
hyperDashDirection = 0;
const float transition_length = 180;
if (HyperDashing)
if (previouslyHyperDashing)
{
this.FadeColour(Color4.OrangeRed, transition_length, Easing.OutQuint);
this.FadeTo(0.2f, transition_length, Easing.OutQuint);
Trail = true;
this.FadeColour(Color4.White, hyperdash_transition_length, Easing.OutQuint);
this.FadeTo(1, hyperdash_transition_length, Easing.OutQuint);
}
else
}
else
{
hyperDashModifier = modifier;
hyperDashDirection = Math.Sign(targetPosition - X);
hyperDashTargetPosition = targetPosition;
if (!previouslyHyperDashing)
{
HyperDashDirection = 0;
this.FadeColour(Color4.White, transition_length, Easing.OutQuint);
this.FadeTo(1, transition_length, Easing.OutQuint);
this.FadeColour(Color4.OrangeRed, hyperdash_transition_length, Easing.OutQuint);
this.FadeTo(0.2f, hyperdash_transition_length, Easing.OutQuint);
Trail = true;
}
}
}
@ -343,12 +360,18 @@ namespace osu.Game.Rulesets.Catch.UI
var direction = Math.Sign(currentDirection);
double dashModifier = Dashing ? 1 : 0.5;
if (hyperDashModifier != 1 && (HyperDashDirection == 0 || direction == Math.Sign(HyperDashDirection)))
dashModifier = hyperDashModifier;
double speed = BASE_SPEED * dashModifier * hyperDashModifier;
Scale = new Vector2(Math.Abs(Scale.X) * direction, Scale.Y);
X = (float)MathHelper.Clamp(X + direction * Clock.ElapsedFrameTime * BASE_SPEED * dashModifier, 0, 1);
X = (float)MathHelper.Clamp(X + direction * Clock.ElapsedFrameTime * speed, 0, 1);
// Correct overshooting.
if (hyperDashDirection > 0 && hyperDashTargetPosition < X ||
hyperDashDirection < 0 && hyperDashTargetPosition > X)
{
X = hyperDashTargetPosition;
SetHyperdashState();
}
}
/// <summary>

View File

@ -12,7 +12,8 @@ using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Mania.Tests
{
internal class ManiaBeatmapConversionTest : BeatmapConversionTest<ConvertValue>
[TestFixture]
public class ManiaBeatmapConversionTest : BeatmapConversionTest<ConvertValue>
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania";
@ -36,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Tests
protected override Ruleset CreateRuleset() => new ManiaRuleset();
}
internal struct ConvertValue : IEquatable<ConvertValue>
public struct ConvertValue : IEquatable<ConvertValue>
{
/// <summary>
/// A sane value to account for osu!stable using ints everwhere.

View File

@ -0,0 +1,45 @@
// 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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests
{
public abstract class ManiaInputTestCase : OsuTestCase
{
private readonly Container<Drawable> content;
protected override Container<Drawable> Content => content ?? base.Content;
protected ManiaInputTestCase(int keys)
{
base.Content.Add(content = new LocalInputManager(keys));
}
private class LocalInputManager : ManiaInputManager
{
public LocalInputManager(int variant)
: base(new ManiaRuleset().RulesetInfo, variant)
{
}
protected override RulesetKeyBindingContainer CreateKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique)
=> new LocalKeyBindingContainer(ruleset, variant, unique);
private class LocalKeyBindingContainer : RulesetKeyBindingContainer
{
public LocalKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique)
: base(ruleset, variant, unique)
{
}
protected override void ReloadMappings()
{
KeyBindings = DefaultKeyBindings;
}
}
}
}
}

View File

@ -0,0 +1,37 @@
// 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.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.UI.Scrolling;
namespace osu.Game.Rulesets.Mania.Tests
{
/// <summary>
/// A container which provides a <see cref="IScrollingInfo"/> to children.
/// </summary>
public class ScrollingTestContainer : Container
{
private readonly ScrollingDirection direction;
public ScrollingTestContainer(ScrollingDirection direction)
{
this.direction = direction;
}
protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = new DependencyContainer(base.CreateLocalDependencies(parent));
dependencies.CacheAs<IScrollingInfo>(new ScrollingInfo { Direction = { Value = direction }});
return dependencies;
}
private class ScrollingInfo : IScrollingInfo
{
public readonly Bindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();
IBindable<ScrollingDirection> IScrollingInfo.Direction => Direction;
}
}
}

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 NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Allocation;
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.Rulesets.Mania.UI;
using osu.Game.Rulesets.Mania.UI.Components;
using osu.Game.Rulesets.UI.Scrolling;
using OpenTK;
using OpenTK.Graphics;
namespace osu.Game.Rulesets.Mania.Tests
{
[TestFixture]
public class TestCaseColumn : ManiaInputTestCase
{
public override IReadOnlyList<Type> RequiredTypes => new[]
{
typeof(Column),
typeof(ColumnBackground),
typeof(ColumnKeyArea),
typeof(ColumnHitObjectArea)
};
private readonly List<Column> columns = new List<Column>();
public TestCaseColumn()
: base(2)
{
}
[BackgroundDependencyLoader]
private void load()
{
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Spacing = new Vector2(20, 0),
Children = new[]
{
createColumn(ScrollingDirection.Up, ManiaAction.Key1),
createColumn(ScrollingDirection.Down, ManiaAction.Key2)
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
AddStep("note", createNote);
AddStep("hold note", createHoldNote);
}
private void createNote()
{
for (int i = 0; i < columns.Count; i++)
{
var obj = new Note { Column = i, StartTime = Time.Current + 2000 };
obj.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
columns[i].Add(new DrawableNote(obj, columns[i].Action));
}
}
private void createHoldNote()
{
for (int i = 0; i < columns.Count; i++)
{
var obj = new HoldNote { Column = i, StartTime = Time.Current + 2000, Duration = 500 };
obj.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
columns[i].Add(new DrawableHoldNote(obj, columns[i].Action));
}
}
private Drawable createColumn(ScrollingDirection direction, ManiaAction action)
{
var column = new Column(direction)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Height = 0.85f,
AccentColour = Color4.OrangeRed,
Action = action,
VisibleTimeRange = { Value = 2000 }
};
columns.Add(column);
return new ScrollingTestContainer(direction)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Y,
Child = column
};
}
}
}

View File

@ -1,106 +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 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;
using OpenTK;
using OpenTK.Graphics;
namespace osu.Game.Rulesets.Mania.Tests
{
[TestFixture]
public class TestCaseManiaHitObjects : OsuTestCase
{
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,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10, 0),
// Imagine that the containers containing the drawable notes are the "columns"
Children = new Drawable[]
{
new Container
{
Name = "Normal note column",
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Y,
Width = 50,
Children = new[]
{
new Container
{
Name = "Timing section",
RelativeSizeAxes = Axes.Both,
RelativeChildSize = new Vector2(1, 10000),
Children = new[]
{
new DrawableNote(note1, ManiaAction.Key1)
{
Y = 5000,
LifetimeStart = double.MinValue,
LifetimeEnd = double.MaxValue,
AccentColour = Color4.Red
},
new DrawableNote(note2, ManiaAction.Key1)
{
Y = 6000,
LifetimeStart = double.MinValue,
LifetimeEnd = double.MaxValue,
AccentColour = Color4.Red
}
}
}
}
},
new Container
{
Name = "Hold note column",
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Y,
Width = 50,
Children = new[]
{
new Container
{
Name = "Timing section",
RelativeSizeAxes = Axes.Both,
RelativeChildSize = new Vector2(1, 10000),
Children = new[]
{
new DrawableHoldNote(holdNote, ManiaAction.Key1)
{
Y = 5000,
Height = 1000,
LifetimeStart = double.MinValue,
LifetimeEnd = double.MaxValue,
AccentColour = Color4.Red,
}
}
}
}
}
}
});
}
}
}

View File

@ -1,185 +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;
using System.Collections.Generic;
using System.Linq;
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;
using osu.Game.Rulesets.Mania.Judgements;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Scoring;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests
{
[TestFixture]
public class TestCaseManiaPlayfield : OsuTestCase
{
private const double start_time = 500;
private const double duration = 500;
protected override double TimePerAction => 200;
private RulesetInfo maniaRuleset;
public TestCaseManiaPlayfield()
{
var rng = new Random(1337);
AddStep("1 column", () => createPlayfield(1));
AddStep("4 columns", () => createPlayfield(4));
AddStep("5 columns", () => createPlayfield(5));
AddStep("8 columns", () => createPlayfield(8));
AddStep("4 + 4 columns", () =>
{
var stages = new List<StageDefinition>
{
new StageDefinition { Columns = 4 },
new StageDefinition { Columns = 4 },
};
createPlayfield(stages);
});
AddStep("2 + 4 + 2 columns", () =>
{
var stages = new List<StageDefinition>
{
new StageDefinition { Columns = 2 },
new StageDefinition { Columns = 4 },
new StageDefinition { Columns = 2 },
};
createPlayfield(stages);
});
AddStep("1 + 8 + 1 columns", () =>
{
var stages = new List<StageDefinition>
{
new StageDefinition { Columns = 1 },
new StageDefinition { Columns = 8 },
new StageDefinition { Columns = 1 },
};
createPlayfield(stages);
});
AddStep("Reversed", () => createPlayfield(4, true));
AddStep("Notes with input", () => createPlayfieldWithNotes());
AddStep("Notes with input (reversed)", () => createPlayfieldWithNotes(true));
AddStep("Notes with gravity", () => createPlayfieldWithNotes());
AddStep("Notes with gravity (reversed)", () => createPlayfieldWithNotes(true));
AddStep("Hit explosion", () =>
{
var playfield = createPlayfield(4);
int col = rng.Next(0, 4);
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(drawableNote, new ManiaJudgement { Result = HitResult.Perfect });
playfield.Columns[col].OnJudgement(drawableNote, new ManiaJudgement { Result = HitResult.Perfect });
});
}
[BackgroundDependencyLoader]
private void load(RulesetStore rulesets, SettingsStore settings)
{
maniaRuleset = rulesets.GetRuleset(3);
Dependencies.Cache(new ManiaConfigManager(settings, maniaRuleset, 4));
}
private ManiaPlayfield createPlayfield(int cols, bool inverted = false)
{
var stages = new List<StageDefinition>
{
new StageDefinition { Columns = cols },
};
return createPlayfield(stages, inverted);
}
private ManiaPlayfield createPlayfield(List<StageDefinition> stages, bool inverted = false)
{
Clear();
var inputManager = new ManiaInputManager(maniaRuleset, stages.Sum(g => g.Columns)) { RelativeSizeAxes = Axes.Both };
Add(inputManager);
ManiaPlayfield playfield;
inputManager.Add(playfield = new ManiaPlayfield(stages)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
});
playfield.Inverted.Value = inverted;
return playfield;
}
private void createPlayfieldWithNotes(bool inverted = false)
{
Clear();
var rateAdjustClock = new StopwatchClock(true) { Rate = 1 };
var inputManager = new ManiaInputManager(maniaRuleset, 4) { RelativeSizeAxes = Axes.Both };
Add(inputManager);
ManiaPlayfield playfield;
var stages = new List<StageDefinition>
{
new StageDefinition { Columns = 4 },
};
inputManager.Add(playfield = new ManiaPlayfield(stages)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Clock = new FramedClock(rateAdjustClock)
});
playfield.Inverted.Value = inverted;
for (double t = start_time; t <= start_time + duration; t += 100)
{
var note1 = new Note { StartTime = t, Column = 0 };
var note2 = new Note { StartTime = t, Column = 3 };
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));
}
var holdNote1 = new HoldNote { StartTime = start_time, Duration = duration, Column = 1 };
var holdNote2 = new HoldNote { StartTime = start_time, Duration = duration, Column = 2 };
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

@ -0,0 +1,168 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
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.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
using OpenTK;
using OpenTK.Graphics;
namespace osu.Game.Rulesets.Mania.Tests
{
[TestFixture]
public class TestCaseNotes : OsuTestCase
{
public override IReadOnlyList<Type> RequiredTypes => new[]
{
typeof(DrawableNote),
typeof(DrawableHoldNote)
};
[BackgroundDependencyLoader]
private void load()
{
Child = new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(20),
Children = new[]
{
createNoteDisplay(ScrollingDirection.Down),
createNoteDisplay(ScrollingDirection.Up),
createHoldNoteDisplay(ScrollingDirection.Down),
createHoldNoteDisplay(ScrollingDirection.Up),
}
};
}
private Drawable createNoteDisplay(ScrollingDirection direction)
{
var note = new Note { StartTime = 999999999 };
note.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
return new ScrollingTestContainer(direction)
{
AutoSizeAxes = Axes.Both,
Child = new NoteContainer(direction, $"note, scrolling {direction.ToString().ToLower()}")
{
Child = new DrawableNote(note, ManiaAction.Key1) { AccentColour = Color4.OrangeRed }
}
};
}
private Drawable createHoldNoteDisplay(ScrollingDirection direction)
{
var note = new HoldNote { StartTime = 999999999, Duration = 1000 };
note.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
return new ScrollingTestContainer(direction)
{
AutoSizeAxes = Axes.Both,
Child = new NoteContainer(direction, $"hold note, scrolling {direction.ToString().ToLower()}")
{
Child = new DrawableHoldNote(note, ManiaAction.Key1)
{
RelativeSizeAxes = Axes.Both,
AccentColour = Color4.OrangeRed,
}
}
};
}
private class NoteContainer : Container
{
private readonly Container content;
protected override Container<Drawable> Content => content;
private readonly ScrollingDirection direction;
public NoteContainer(ScrollingDirection direction, string description)
{
this.direction = direction;
AutoSizeAxes = Axes.Both;
InternalChild = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(0, 10),
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new Container
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Width = 45,
Height = 100,
Children = new Drawable[]
{
new Box
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.Both,
Width = 1.25f,
Colour = Color4.Black.Opacity(0.5f)
},
content = new Container { RelativeSizeAxes = Axes.Both }
}
},
new SpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
TextSize = 14,
Text = description
}
}
};
}
protected override void Update()
{
base.Update();
foreach (var obj in content.OfType<DrawableHitObject>())
{
if (!(obj.HitObject is IHasEndTime endTime))
continue;
if (!obj.HasNestedHitObjects)
continue;
foreach (var nested in obj.NestedHitObjects)
{
double finalPosition = (nested.HitObject.StartTime - obj.HitObject.StartTime) / endTime.Duration;
switch (direction)
{
case ScrollingDirection.Up:
nested.Y = (float)(finalPosition * content.DrawHeight);
break;
case ScrollingDirection.Down:
nested.Y = (float)(-finalPosition * content.DrawHeight);
break;
}
}
}
}
}
}
}

View File

@ -0,0 +1,121 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.UI.Scrolling;
using OpenTK;
namespace osu.Game.Rulesets.Mania.Tests
{
[TestFixture]
public class TestCaseStage : ManiaInputTestCase
{
private const int columns = 4;
private readonly List<ManiaStage> stages = new List<ManiaStage>();
public TestCaseStage()
: base(columns)
{
}
[BackgroundDependencyLoader]
private void load()
{
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Spacing = new Vector2(20, 0),
Children = new[]
{
createStage(ScrollingDirection.Up, ManiaAction.Key1),
createStage(ScrollingDirection.Down, ManiaAction.Key3)
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
AddStep("note", createNote);
AddStep("hold note", createHoldNote);
AddStep("minor bar line", () => createBarLine(false));
AddStep("major bar line", () => createBarLine(true));
}
private void createNote()
{
foreach (var stage in stages)
{
for (int i = 0; i < stage.Columns.Count; i++)
{
var obj = new Note { Column = i, StartTime = Time.Current + 2000 };
obj.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
stage.Add(new DrawableNote(obj, stage.Columns[i].Action));
}
}
}
private void createHoldNote()
{
foreach (var stage in stages)
{
for (int i = 0; i < stage.Columns.Count; i++)
{
var obj = new HoldNote { Column = i, StartTime = Time.Current + 2000, Duration = 500 };
obj.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
stage.Add(new DrawableHoldNote(obj, stage.Columns[i].Action));
}
}
}
private void createBarLine(bool major)
{
foreach (var stage in stages)
{
var obj = new BarLine
{
StartTime = Time.Current + 2000,
ControlPoint = new TimingControlPoint(),
BeatIndex = major ? 0 : 1
};
obj.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
stage.Add(obj);
}
}
private Drawable createStage(ScrollingDirection direction, ManiaAction action)
{
var specialAction = ManiaAction.Special1;
var stage = new ManiaStage(direction, 0, new StageDefinition { Columns = 2 }, ref action, ref specialAction) { VisibleTimeRange = { Value = 2000 } };
stages.Add(stage);
return new ScrollingTestContainer(direction)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Child = stage
};
}
}
}

View File

@ -329,7 +329,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
break;
}
bool isDoubleSample(SampleInfo sample) => sample.Name == SampleInfo.HIT_CLAP && sample.Name == SampleInfo.HIT_FINISH;
bool isDoubleSample(SampleInfo sample) => sample.Name == SampleInfo.HIT_CLAP || sample.Name == SampleInfo.HIT_FINISH;
bool canGenerateTwoNotes = (convertType & PatternType.LowProbability) == 0;
canGenerateTwoNotes &= HitObject.Samples.Any(isDoubleSample) || sampleInfoListAt(HitObject.StartTime).Any(isDoubleSample);

View File

@ -4,6 +4,7 @@
using osu.Framework.Configuration.Tracking;
using osu.Game.Configuration;
using osu.Game.Rulesets.Configuration;
using osu.Game.Rulesets.Mania.UI;
namespace osu.Game.Rulesets.Mania.Configuration
{
@ -19,6 +20,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
base.InitialiseDefaults();
Set(ManiaSetting.ScrollTime, 1500.0, 50.0, 10000.0, 50.0);
Set(ManiaSetting.ScrollDirection, ManiaScrollingDirection.Down);
}
public override TrackedSettings CreateTrackedSettings() => new TrackedSettings
@ -29,6 +31,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
public enum ManiaSetting
{
ScrollTime
ScrollTime,
ScrollDirection
}
}

View File

@ -0,0 +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.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Mania.Difficulty
{
public class ManiaDifficultyAttributes : DifficultyAttributes
{
public double GreatHitWindow;
public ManiaDifficultyAttributes(Mod[] mods, double starRating)
: base(mods, starRating)
{
}
}
}

View File

@ -39,6 +39,9 @@ namespace osu.Game.Rulesets.Mania.Difficulty
protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double timeRate)
{
if (!beatmap.HitObjects.Any())
return new ManiaDifficultyAttributes(mods, 0);
var difficultyHitObjects = new List<ManiaHitObjectDifficulty>();
int columnCount = ((ManiaBeatmap)beatmap).TotalColumns;
@ -50,9 +53,14 @@ namespace osu.Game.Rulesets.Mania.Difficulty
if (!calculateStrainValues(difficultyHitObjects, timeRate))
return new DifficultyAttributes(mods, 0);
double starRating = calculateDifficulty(difficultyHitObjects, timeRate) * star_scaling_factor;
return new DifficultyAttributes(mods, starRating);
return new ManiaDifficultyAttributes(mods, starRating)
{
// Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be remoevd in the future
GreatHitWindow = (int)(beatmap.HitObjects.First().HitWindows.Great / 2) / timeRate
};
}
private bool calculateStrainValues(List<ManiaHitObjectDifficulty> objects, double timeRate)

View File

@ -13,6 +13,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty
{
public class ManiaPerformanceCalculator : PerformanceCalculator
{
protected new ManiaDifficultyAttributes Attributes => (ManiaDifficultyAttributes)base.Attributes;
private Mod[] mods;
// Score after being scaled by non-difficulty-increasing mods
@ -105,14 +107,12 @@ namespace osu.Game.Rulesets.Mania.Difficulty
private double computeAccuracyValue(double strainValue)
{
// Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be remoevd in the future
double hitWindowGreat = (int)(Beatmap.HitObjects.First().HitWindows.Great / 2) / TimeRate;
if (hitWindowGreat <= 0)
if (Attributes.GreatHitWindow <= 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)
double accuracyValue = Math.Max(0.0, 0.2 - (Attributes.GreatHitWindow - 34) * 0.006667)
* strainValue
* Math.Pow(Math.Max(0.0, scaledScore - 960000) / 40000, 1.1);

View File

@ -16,6 +16,7 @@ using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Replays.Types;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Configuration;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Configuration;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mania.Beatmaps;
@ -155,6 +156,8 @@ namespace osu.Game.Rulesets.Mania
public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new ManiaConfigManager(settings, RulesetInfo);
public override RulesetSettingsSubsection CreateSettings() => new ManiaSettingsSubsection(this);
public ManiaRuleset(RulesetInfo rulesetInfo = null)
: base(rulesetInfo)
{

View File

@ -0,0 +1,34 @@
// 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.Allocation;
using osu.Framework.Graphics;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.UI;
namespace osu.Game.Rulesets.Mania
{
public class ManiaSettingsSubsection : RulesetSettingsSubsection
{
protected override string Header => "osu!mania";
public ManiaSettingsSubsection(ManiaRuleset ruleset)
: base(ruleset)
{
}
[BackgroundDependencyLoader]
private void load(ManiaConfigManager config)
{
Children = new Drawable[]
{
new SettingsEnumDropdown<ManiaScrollingDirection>
{
LabelText = "Scrolling direction",
Bindable = config.GetBindable<ManiaScrollingDirection>(ManiaSetting.ScrollDirection)
}
};
}
}
}

View File

@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Mania.Judgements;
using osu.Framework.Input.Bindings;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI.Scrolling;
namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
@ -75,6 +76,13 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
AddNested(tail);
}
protected override void OnDirectionChanged(ScrollingDirection direction)
{
base.OnDirectionChanged(direction);
bodyPiece.Anchor = bodyPiece.Origin = direction == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft;
}
public override Color4 AccentColour
{
get { return base.AccentColour; }
@ -100,7 +108,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
base.Update();
// Make the body piece not lie under the head note
bodyPiece.Y = head.Height / 2;
bodyPiece.Y = (Direction.Value == ScrollingDirection.Up ? 1 : -1) * head.Height / 2;
bodyPiece.Height = DrawHeight - head.Height / 2 + tail.Height / 2;
}

View File

@ -1,8 +1,12 @@
// 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.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
@ -16,18 +20,29 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
public new TObject HitObject;
protected readonly IBindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();
protected DrawableManiaHitObject(TObject hitObject, ManiaAction? action = null)
: base(hitObject)
{
Anchor = Anchor.TopCentre;
Origin = Anchor.TopCentre;
HitObject = hitObject;
if (action != null)
Action = action.Value;
}
[BackgroundDependencyLoader]
private void load(IScrollingInfo scrollingInfo)
{
Direction.BindTo(scrollingInfo.Direction);
Direction.BindValueChanged(OnDirectionChanged, true);
}
protected virtual void OnDirectionChanged(ScrollingDirection direction)
{
Anchor = Origin = direction == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
}
protected override void UpdateState(ArmedState state)
{
switch (state)

View File

@ -9,6 +9,7 @@ using osu.Framework.Input.Bindings;
using osu.Game.Rulesets.Mania.Judgements;
using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI.Scrolling;
namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
@ -28,14 +29,14 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
CornerRadius = 5;
Masking = true;
InternalChildren = new Drawable[]
{
headPiece = new NotePiece
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre
}
};
InternalChild = headPiece = new NotePiece();
}
protected override void OnDirectionChanged(ScrollingDirection direction)
{
base.OnDirectionChanged(direction);
headPiece.Anchor = headPiece.Origin = direction == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
}
public override Color4 AccentColour

View File

@ -1,12 +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.Framework.Allocation;
using osu.Framework.Configuration;
using OpenTK.Graphics;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.UI.Scrolling;
namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
{
@ -18,6 +22,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
public const float NOTE_HEIGHT = 10;
private const float head_colour_height = 6;
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
private readonly Box colouredBox;
public NotePiece()
@ -33,8 +39,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
},
colouredBox = new Box
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.X,
Height = head_colour_height,
Alpha = 0.2f
@ -42,6 +46,16 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
};
}
[BackgroundDependencyLoader]
private void load(IScrollingInfo scrollingInfo)
{
direction.BindTo(scrollingInfo.Direction);
direction.BindValueChanged(direction =>
{
colouredBox.Anchor = colouredBox.Origin = direction == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
}, true);
}
private Color4 accentColour;
public Color4 AccentColour
{

View File

@ -17,6 +17,6 @@ namespace osu.Game.Rulesets.Mania.Replays
protected override bool IsImportant(ManiaReplayFrame frame) => frame.Actions.Any();
public override List<InputState> GetPendingStates() => new List<InputState> { new ReplayState<ManiaAction> { PressedActions = CurrentFrame.Actions } };
public override List<IInput> GetPendingInputs() => new List<IInput> { new ReplayState<ManiaAction> { PressedActions = CurrentFrame.Actions } };
}
}

View File

@ -1,50 +1,51 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using OpenTK;
using OpenTK.Graphics;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Colour;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using System;
using System.Linq;
using osu.Framework.Input.Bindings;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.UI.Components;
using osu.Game.Rulesets.UI.Scrolling;
namespace osu.Game.Rulesets.Mania.UI
{
public class Column : ScrollingPlayfield, IKeyBindingHandler<ManiaAction>, IHasAccentColour
public class Column : ManiaScrollingPlayfield, IKeyBindingHandler<ManiaAction>, IHasAccentColour
{
private const float key_icon_size = 10;
private const float key_icon_corner_radius = 3;
private const float key_icon_border_radius = 2;
private const float hit_target_height = 10;
private const float hit_target_bar_height = 2;
private const float column_width = 45;
private const float special_column_width = 70;
public ManiaAction Action;
private ManiaAction action;
private readonly Box background;
private readonly Box backgroundOverlay;
private readonly Container hitTargetBar;
private readonly Container keyIcon;
public ManiaAction Action
{
get => action;
set
{
if (action == value)
return;
action = value;
background.Action = value;
keyArea.Action = value;
}
}
private readonly ColumnBackground background;
private readonly ColumnKeyArea keyArea;
private readonly ColumnHitObjectArea hitObjectArea;
internal readonly Container TopLevelContainer;
private readonly Container explosionContainer;
protected override Container<Drawable> Content => content;
private readonly Container<Drawable> content;
protected override Container<Drawable> Content => hitObjectArea;
public Column()
: base(ScrollingDirection.Up)
public Column(ScrollingDirection direction)
: base(direction)
{
RelativeSizeAxes = Axes.Y;
Width = column_width;
@ -52,71 +53,21 @@ namespace osu.Game.Rulesets.Mania.UI
Masking = true;
CornerRadius = 5;
InternalChildren = new Drawable[]
background = new ColumnBackground { RelativeSizeAxes = Axes.Both };
Container hitTargetContainer;
InternalChildren = new[]
{
background = new Box
{
Name = "Background",
RelativeSizeAxes = Axes.Both,
Alpha = 0.3f
},
backgroundOverlay = new Box
{
Name = "Background Gradient Overlay",
RelativeSizeAxes = Axes.Both,
Height = 0.5f,
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
Blending = BlendingMode.Additive,
Alpha = 0
},
new Container
// For input purposes, the background is added at the highest depth, but is then proxied back below all other elements
background.CreateProxy(),
hitTargetContainer = new Container
{
Name = "Hit target + hit objects",
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Top = ManiaStage.HIT_TARGET_POSITION },
Children = new Drawable[]
{
new Container
{
Name = "Hit target",
RelativeSizeAxes = Axes.X,
Height = hit_target_height,
Children = new Drawable[]
{
new Box
{
Name = "Background",
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black
},
hitTargetBar = new Container
{
Name = "Bar",
RelativeSizeAxes = Axes.X,
Height = hit_target_bar_height,
Masking = true,
Children = new[]
{
new Box
{
RelativeSizeAxes = Axes.Both
}
}
}
}
},
content = new Container
{
Name = "Hit objects",
RelativeSizeAxes = Axes.Both,
},
// For column lighting, we need to capture input events before the notes
new InputTarget
{
Pressed = onPressed,
Released = onReleased
},
hitObjectArea = new ColumnHitObjectArea { RelativeSizeAxes = Axes.Both },
explosionContainer = new Container
{
Name = "Hit explosions",
@ -124,46 +75,27 @@ namespace osu.Game.Rulesets.Mania.UI
}
}
},
new Container
keyArea = new ColumnKeyArea
{
Name = "Key",
RelativeSizeAxes = Axes.X,
Height = ManiaStage.HIT_TARGET_POSITION,
Children = new Drawable[]
{
new Box
{
Name = "Key gradient",
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientVertical(Color4.Black, Color4.Black.Opacity(0)),
Alpha = 0.5f
},
keyIcon = new Container
{
Name = "Key icon",
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(key_icon_size),
Masking = true,
CornerRadius = key_icon_corner_radius,
BorderThickness = 2,
BorderColour = Color4.White, // Not true
Children = new[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
AlwaysPresent = true
}
}
}
}
},
background,
TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both }
};
TopLevelContainer.Add(explosionContainer.CreateProxy());
Direction.BindValueChanged(d =>
{
hitTargetContainer.Padding = new MarginPadding
{
Top = d == ScrollingDirection.Up ? ManiaStage.HIT_TARGET_POSITION : 0,
Bottom = d == ScrollingDirection.Down ? ManiaStage.HIT_TARGET_POSITION : 0,
};
keyArea.Anchor = keyArea.Origin= d == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft;
}, true);
}
public override Axes RelativeSizeAxes => Axes.Y;
@ -192,22 +124,9 @@ namespace osu.Game.Rulesets.Mania.UI
return;
accentColour = value;
background.Colour = accentColour;
backgroundOverlay.Colour = ColourInfo.GradientVertical(accentColour.Opacity(0.6f), accentColour.Opacity(0));
hitTargetBar.EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Radius = 5,
Colour = accentColour.Opacity(0.5f),
};
keyIcon.EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Radius = 5,
Colour = accentColour.Opacity(0.5f),
};
background.AccentColour = value;
keyArea.AccentColour = value;
hitObjectArea.AccentColour = value;
}
}
@ -228,48 +147,10 @@ namespace osu.Game.Rulesets.Mania.UI
if (!judgement.IsHit || !judgedObject.DisplayJudgement)
return;
explosionContainer.Add(new HitExplosion(judgedObject));
}
private bool onPressed(ManiaAction action)
{
if (action == Action)
explosionContainer.Add(new HitExplosion(judgedObject)
{
backgroundOverlay.FadeTo(1, 50, Easing.OutQuint).Then().FadeTo(0.5f, 250, Easing.OutQuint);
keyIcon.ScaleTo(1.4f, 50, Easing.OutQuint).Then().ScaleTo(1.3f, 250, Easing.OutQuint);
}
return false;
}
private bool onReleased(ManiaAction action)
{
if (action == Action)
{
backgroundOverlay.FadeTo(0, 250, Easing.OutQuint);
keyIcon.ScaleTo(1f, 125, Easing.OutQuint);
}
return false;
}
/// <summary>
/// This is a simple container which delegates various input events that have to be captured before the notes.
/// </summary>
private class InputTarget : Container, IKeyBindingHandler<ManiaAction>
{
public Func<ManiaAction, bool> Pressed;
public Func<ManiaAction, bool> Released;
public InputTarget()
{
RelativeSizeAxes = Axes.Both;
AlwaysPresent = true;
Alpha = 0;
}
public bool OnPressed(ManiaAction action) => Pressed?.Invoke(action) ?? false;
public bool OnReleased(ManiaAction action) => Released?.Invoke(action) ?? false;
Anchor = Direction == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre
});
}
public bool OnPressed(ManiaAction action)

View File

@ -0,0 +1,106 @@
// 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.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Bindings;
using osu.Game.Graphics;
using osu.Game.Rulesets.UI.Scrolling;
using OpenTK.Graphics;
namespace osu.Game.Rulesets.Mania.UI.Components
{
public class ColumnBackground : CompositeDrawable, IKeyBindingHandler<ManiaAction>, IHasAccentColour
{
public ManiaAction Action;
private Box background;
private Box backgroundOverlay;
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
[BackgroundDependencyLoader]
private void load(IScrollingInfo scrollingInfo)
{
InternalChildren = new[]
{
background = new Box
{
Name = "Background",
RelativeSizeAxes = Axes.Both,
Alpha = 0.3f
},
backgroundOverlay = new Box
{
Name = "Background Gradient Overlay",
RelativeSizeAxes = Axes.Both,
Height = 0.5f,
Blending = BlendingMode.Additive,
Alpha = 0
}
};
direction.BindTo(scrollingInfo.Direction);
direction.BindValueChanged(direction =>
{
backgroundOverlay.Anchor = backgroundOverlay.Origin = direction == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft;
updateColours();
}, true);
}
protected override void LoadComplete()
{
base.LoadComplete();
updateColours();
}
private Color4 accentColour;
public Color4 AccentColour
{
get => accentColour;
set
{
if (accentColour == value)
return;
accentColour = value;
updateColours();
}
}
private void updateColours()
{
if (!IsLoaded)
return;
background.Colour = AccentColour;
var brightPoint = AccentColour.Opacity(0.6f);
var dimPoint = AccentColour.Opacity(0);
backgroundOverlay.Colour = ColourInfo.GradientVertical(
direction.Value == ScrollingDirection.Up ? brightPoint : dimPoint,
direction.Value == ScrollingDirection.Up ? dimPoint : brightPoint);
}
public bool OnPressed(ManiaAction action)
{
if (action == Action)
backgroundOverlay.FadeTo(1, 50, Easing.OutQuint).Then().FadeTo(0.5f, 250, Easing.OutQuint);
return false;
}
public bool OnReleased(ManiaAction action)
{
if (action == Action)
backgroundOverlay.FadeTo(0, 250, Easing.OutQuint);
return false;
}
}
}

View File

@ -0,0 +1,99 @@
// 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.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Rulesets.UI.Scrolling;
using OpenTK.Graphics;
namespace osu.Game.Rulesets.Mania.UI.Components
{
public class ColumnHitObjectArea : Container, IHasAccentColour
{
private const float hit_target_height = 10;
private const float hit_target_bar_height = 2;
private Container<Drawable> content;
protected override Container<Drawable> Content => content;
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
private Container hitTargetLine;
[BackgroundDependencyLoader]
private void load(IScrollingInfo scrollingInfo)
{
Drawable hitTargetBar;
InternalChildren = new[]
{
hitTargetBar = new Box
{
RelativeSizeAxes = Axes.X,
Height = hit_target_height,
Colour = Color4.Black
},
hitTargetLine = new Container
{
RelativeSizeAxes = Axes.X,
Height = hit_target_bar_height,
Masking = true,
Child = new Box { RelativeSizeAxes = Axes.Both }
},
content = new Container
{
Name = "Hit objects",
RelativeSizeAxes = Axes.Both,
},
};
direction.BindTo(scrollingInfo.Direction);
direction.BindValueChanged(direction =>
{
Anchor anchor = direction == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft;
hitTargetBar.Anchor = hitTargetBar.Origin = anchor;
hitTargetLine.Anchor = hitTargetLine.Origin = anchor;
}, true);
}
protected override void LoadComplete()
{
base.LoadComplete();
updateColours();
}
private Color4 accentColour;
public Color4 AccentColour
{
get => accentColour;
set
{
if (accentColour == value)
return;
accentColour = value;
updateColours();
}
}
private void updateColours()
{
if (!IsLoaded)
return;
hitTargetLine.EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Radius = 5,
Colour = accentColour.Opacity(0.5f),
};
}
}
}

View File

@ -0,0 +1,122 @@
// 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.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Bindings;
using osu.Game.Graphics;
using osu.Game.Rulesets.UI.Scrolling;
using OpenTK;
using OpenTK.Graphics;
namespace osu.Game.Rulesets.Mania.UI.Components
{
public class ColumnKeyArea : CompositeDrawable, IKeyBindingHandler<ManiaAction>, IHasAccentColour
{
private const float key_icon_size = 10;
private const float key_icon_corner_radius = 3;
public ManiaAction Action;
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
private Container keyIcon;
[BackgroundDependencyLoader]
private void load(IScrollingInfo scrollingInfo)
{
Drawable gradient;
InternalChildren = new[]
{
gradient = new Box
{
Name = "Key gradient",
RelativeSizeAxes = Axes.Both,
Alpha = 0.5f
},
keyIcon = new Container
{
Name = "Key icon",
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(key_icon_size),
Masking = true,
CornerRadius = key_icon_corner_radius,
BorderThickness = 2,
BorderColour = Color4.White, // Not true
Children = new[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
AlwaysPresent = true
}
}
}
};
direction.BindTo(scrollingInfo.Direction);
direction.BindValueChanged(direction =>
{
gradient.Colour = ColourInfo.GradientVertical(
direction == ScrollingDirection.Up ? Color4.Black : Color4.Black.Opacity(0),
direction == ScrollingDirection.Up ? Color4.Black.Opacity(0) : Color4.Black);
}, true);
}
protected override void LoadComplete()
{
base.LoadComplete();
updateColours();
}
private Color4 accentColour;
public Color4 AccentColour
{
get => accentColour;
set
{
if (accentColour == value)
return;
accentColour = value;
updateColours();
}
}
private void updateColours()
{
if (!IsLoaded)
return;
keyIcon.EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Radius = 5,
Colour = accentColour.Opacity(0.5f),
};
}
public bool OnPressed(ManiaAction action)
{
if (action == Action)
keyIcon.ScaleTo(1.4f, 50, Easing.OutQuint).Then().ScaleTo(1.3f, 250, Easing.OutQuint);
return false;
}
public bool OnReleased(ManiaAction action)
{
if (action == Action)
keyIcon.ScaleTo(1f, 125, Easing.OutQuint);
return false;
}
}
}

View File

@ -23,7 +23,6 @@ namespace osu.Game.Rulesets.Mania.UI
{
bool isTick = judgedObject is DrawableHoldNoteTick;
Anchor = Anchor.TopCentre;
Origin = Anchor.Centre;
RelativeSizeAxes = Axes.X;

View File

@ -0,0 +1,17 @@
// 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.Configuration;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI.Scrolling;
namespace osu.Game.Rulesets.Mania.UI
{
public interface IScrollingInfo
{
/// <summary>
/// The direction <see cref="HitObject"/>s should scroll in.
/// </summary>
IBindable<ScrollingDirection> Direction { get; }
}
}

View File

@ -8,7 +8,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Objects.Drawables;
@ -17,18 +16,13 @@ using osu.Game.Rulesets.UI.Scrolling;
namespace osu.Game.Rulesets.Mania.UI
{
public class ManiaPlayfield : ScrollingPlayfield
public class ManiaPlayfield : ManiaScrollingPlayfield
{
/// <summary>
/// Whether this playfield should be inverted. This flips everything inside the playfield.
/// </summary>
public readonly Bindable<bool> Inverted = new Bindable<bool>(true);
public List<Column> Columns => stages.SelectMany(x => x.Columns).ToList();
private readonly List<ManiaStage> stages = new List<ManiaStage>();
public ManiaPlayfield(List<StageDefinition> stageDefinitions)
: base(ScrollingDirection.Up)
public ManiaPlayfield(ScrollingDirection direction, List<StageDefinition> stageDefinitions)
: base(direction)
{
if (stageDefinitions == null)
throw new ArgumentNullException(nameof(stageDefinitions));
@ -36,8 +30,6 @@ namespace osu.Game.Rulesets.Mania.UI
if (stageDefinitions.Count <= 0)
throw new ArgumentException("Can't have zero or fewer stages.");
Inverted.Value = true;
GridContainer playfieldGrid;
InternalChild = playfieldGrid = new GridContainer
{
@ -50,9 +42,8 @@ namespace osu.Game.Rulesets.Mania.UI
int firstColumnIndex = 0;
for (int i = 0; i < stageDefinitions.Count; i++)
{
var newStage = new ManiaStage(firstColumnIndex, stageDefinitions[i], ref normalColumnAction, ref specialColumnAction);
var newStage = new ManiaStage(direction, firstColumnIndex, stageDefinitions[i], ref normalColumnAction, ref specialColumnAction);
newStage.VisibleTimeRange.BindTo(VisibleTimeRange);
newStage.Inverted.BindTo(Inverted);
playfieldGrid.Content[0][i] = newStage;

View File

@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Input;
@ -12,6 +13,7 @@ using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Input.Handlers;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
@ -33,6 +35,9 @@ namespace osu.Game.Rulesets.Mania.UI
public IEnumerable<BarLine> BarLines;
private readonly Bindable<ManiaScrollingDirection> configDirection = new Bindable<ManiaScrollingDirection>();
private ScrollingInfo scrollingInfo;
public ManiaRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap)
{
@ -65,12 +70,24 @@ namespace osu.Game.Rulesets.Mania.UI
}
[BackgroundDependencyLoader]
private void load()
private void load(ManiaConfigManager config)
{
BarLines.ForEach(Playfield.Add);
config.BindWith(ManiaSetting.ScrollDirection, configDirection);
configDirection.BindValueChanged(d => scrollingInfo.Direction.Value = (ScrollingDirection)d, true);
}
protected sealed override Playfield CreatePlayfield() => new ManiaPlayfield(Beatmap.Stages)
private DependencyContainer dependencies;
protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent)
{
dependencies = new DependencyContainer(base.CreateLocalDependencies(parent));
dependencies.CacheAs<IScrollingInfo>(scrollingInfo = new ScrollingInfo());
return dependencies;
}
protected sealed override Playfield CreatePlayfield() => new ManiaPlayfield(scrollingInfo.Direction, Beatmap.Stages)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@ -100,5 +117,11 @@ namespace osu.Game.Rulesets.Mania.UI
protected override Vector2 PlayfieldArea => new Vector2(1, 0.8f);
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new ManiaFramedReplayInputHandler(replay);
private class ScrollingInfo : IScrollingInfo
{
public readonly Bindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();
IBindable<ScrollingDirection> IScrollingInfo.Direction => Direction;
}
}
}

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.UI.Scrolling;
namespace osu.Game.Rulesets.Mania.UI
{
public enum ManiaScrollingDirection
{
Up = ScrollingDirection.Up,
Down = ScrollingDirection.Down
}
}

View File

@ -0,0 +1,26 @@
// 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.Allocation;
using osu.Framework.Configuration;
using osu.Game.Rulesets.UI.Scrolling;
namespace osu.Game.Rulesets.Mania.UI
{
public class ManiaScrollingPlayfield : ScrollingPlayfield
{
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
public ManiaScrollingPlayfield(ScrollingDirection direction)
: base(direction)
{
}
[BackgroundDependencyLoader]
private void load(IScrollingInfo scrollingInfo)
{
direction.BindTo(scrollingInfo.Direction);
direction.BindValueChanged(direction => Direction.Value = direction, true);
}
}
}

View File

@ -5,7 +5,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@ -24,20 +23,15 @@ namespace osu.Game.Rulesets.Mania.UI
/// <summary>
/// A collection of <see cref="Column"/>s.
/// </summary>
internal class ManiaStage : ScrollingPlayfield
internal class ManiaStage : ManiaScrollingPlayfield
{
public const float HIT_TARGET_POSITION = 50;
/// <summary>
/// Whether this playfield should be inverted. This flips everything inside the playfield.
/// </summary>
public readonly Bindable<bool> Inverted = new Bindable<bool>(true);
public IReadOnlyList<Column> Columns => columnFlow.Children;
private readonly FillFlowContainer<Column> columnFlow;
protected override Container<Drawable> Content => content;
private readonly Container<Drawable> content;
protected override Container<Drawable> Content => barLineContainer;
private readonly Container<Drawable> barLineContainer;
public Container<DrawableManiaJudgement> Judgements => judgements;
private readonly JudgementContainer<DrawableManiaJudgement> judgements;
@ -49,8 +43,8 @@ namespace osu.Game.Rulesets.Mania.UI
private readonly int firstColumnIndex;
public ManiaStage(int firstColumnIndex, StageDefinition definition, ref ManiaAction normalColumnStartAction, ref ManiaAction specialColumnStartAction)
: base(ScrollingDirection.Up)
public ManiaStage(ScrollingDirection direction, int firstColumnIndex, StageDefinition definition, ref ManiaAction normalColumnStartAction, ref ManiaAction specialColumnStartAction)
: base(direction)
{
this.firstColumnIndex = firstColumnIndex;
@ -106,13 +100,12 @@ namespace osu.Game.Rulesets.Mania.UI
Width = 1366, // Bar lines should only be masked on the vertical axis
BypassAutoSizeAxes = Axes.Both,
Masking = true,
Child = content = new Container
Child = barLineContainer = new Container
{
Name = "Bar lines",
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.Y,
Padding = new MarginPadding { Top = HIT_TARGET_POSITION }
}
},
judgements = new JudgementContainer<DrawableManiaJudgement>
@ -131,7 +124,7 @@ namespace osu.Game.Rulesets.Mania.UI
for (int i = 0; i < definition.Columns; i++)
{
var isSpecial = definition.IsSpecialColumn(i);
var column = new Column
var column = new Column(direction)
{
IsSpecial = isSpecial,
Action = isSpecial ? specialColumnStartAction++ : normalColumnStartAction++
@ -140,14 +133,14 @@ namespace osu.Game.Rulesets.Mania.UI
AddColumn(column);
}
Inverted.ValueChanged += invertedChanged;
Inverted.TriggerChange();
}
private void invertedChanged(bool newValue)
{
Scale = new Vector2(1, newValue ? -1 : 1);
Judgements.Scale = Scale;
Direction.BindValueChanged(d =>
{
barLineContainer.Padding = new MarginPadding
{
Top = d == ScrollingDirection.Up ? HIT_TARGET_POSITION : 0,
Bottom = d == ScrollingDirection.Down ? HIT_TARGET_POSITION : 0,
};
}, true);
}
public void AddColumn(Column c)
@ -218,7 +211,7 @@ namespace osu.Game.Rulesets.Mania.UI
{
// Due to masking differences, it is not possible to get the width of the columns container automatically
// While masking on effectively only the Y-axis, so we need to set the width of the bar line container manually
content.Width = columnFlow.Width;
barLineContainer.Width = columnFlow.Width;
}
}
}

View File

@ -8,17 +8,19 @@ using osu.Framework.MathUtils;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Tests.Beatmaps;
using OpenTK;
namespace osu.Game.Rulesets.Osu.Tests
{
internal class OsuBeatmapConversionTest : BeatmapConversionTest<ConvertValue>
[TestFixture]
public class OsuBeatmapConversionTest : BeatmapConversionTest<ConvertValue>
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
[TestCase("basic")]
[TestCase("colinear-perfect-curve")]
[TestCase("slider-ticks")]
public new void Test(string name)
{
base.Test(name);
@ -26,24 +28,30 @@ namespace osu.Game.Rulesets.Osu.Tests
protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject)
{
var startPosition = (hitObject as IHasPosition)?.Position ?? new Vector2(256, 192);
var endPosition = (hitObject as Slider)?.EndPosition ?? startPosition;
yield return new ConvertValue
switch (hitObject)
{
StartTime = hitObject.StartTime,
EndTime = (hitObject as IHasEndTime)?.EndTime ?? hitObject.StartTime,
StartX = startPosition.X,
StartY = startPosition.Y,
EndX = endPosition.X,
EndY = endPosition.Y
case Slider slider:
foreach (var nested in slider.NestedHitObjects)
yield return createConvertValue(nested);
break;
default:
yield return createConvertValue(hitObject);
break;
}
ConvertValue createConvertValue(HitObject obj) => new ConvertValue
{
StartTime = obj.StartTime,
EndTime = (obj as IHasEndTime)?.EndTime ?? obj.StartTime,
X = (obj as IHasPosition)?.X ?? OsuPlayfield.BASE_SIZE.X / 2,
Y = (obj as IHasPosition)?.Y ?? OsuPlayfield.BASE_SIZE.Y / 2,
};
}
protected override Ruleset CreateRuleset() => new OsuRuleset();
}
internal struct ConvertValue : IEquatable<ConvertValue>
public struct ConvertValue : IEquatable<ConvertValue>
{
/// <summary>
/// A sane value to account for osu!stable using ints everwhere.
@ -52,17 +60,13 @@ namespace osu.Game.Rulesets.Osu.Tests
public double StartTime;
public double EndTime;
public float StartX;
public float StartY;
public float EndX;
public float EndY;
public float X;
public float Y;
public bool Equals(ConvertValue other)
=> Precision.AlmostEquals(StartTime, other.StartTime)
=> Precision.AlmostEquals(StartTime, other.StartTime, conversion_lenience)
&& Precision.AlmostEquals(EndTime, other.EndTime, conversion_lenience)
&& Precision.AlmostEquals(StartX, other.StartX)
&& Precision.AlmostEquals(StartY, other.StartY, conversion_lenience)
&& Precision.AlmostEquals(EndX, other.EndX, conversion_lenience)
&& Precision.AlmostEquals(EndY, other.EndY, conversion_lenience);
&& Precision.AlmostEquals(X, other.X, conversion_lenience)
&& Precision.AlmostEquals(Y, other.Y, conversion_lenience);
}
}

View File

@ -27,6 +27,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
var endTimeData = original as IHasEndTime;
var positionData = original as IHasPosition;
var comboData = original as IHasCombo;
var legacyOffset = original as IHasLegacyLastTickOffset;
if (curveData != null)
{
@ -40,7 +41,8 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
RepeatSamples = curveData.RepeatSamples,
RepeatCount = curveData.RepeatCount,
Position = positionData?.Position ?? Vector2.Zero,
NewCombo = comboData?.NewCombo ?? false
NewCombo = comboData?.NewCombo ?? false,
LegacyLastTickOffset = legacyOffset?.LegacyLastTickOffset
};
}
else if (endTimeData != null)

View File

@ -10,6 +10,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{
public double AimStrain;
public double SpeedStrain;
public double ApproachRate;
public double OverallDifficulty;
public int MaxCombo;
public OsuDifficultyAttributes(Mod[] mods, double starRating)
: base(mods, starRating)

View File

@ -25,6 +25,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double timeRate)
{
if (!beatmap.HitObjects.Any())
return new OsuDifficultyAttributes(mods, 0);
OsuDifficultyBeatmap difficultyBeatmap = new OsuDifficultyBeatmap(beatmap.HitObjects.Cast<OsuHitObject>().ToList(), timeRate);
Skill[] skills =
{
@ -58,10 +61,21 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double speedRating = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier;
double starRating = aimRating + speedRating + Math.Abs(aimRating - speedRating) / 2;
// Todo: These int casts are temporary to achieve 1:1 results with osu!stable, and should be remoevd in the future
double hitWindowGreat = (int)(beatmap.HitObjects.First().HitWindows.Great / 2) / timeRate;
double preEmpt = (int)BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / timeRate;
int maxCombo = 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)
maxCombo += beatmap.HitObjects.OfType<Slider>().Sum(s => s.NestedHitObjects.Count - 1);
return new OsuDifficultyAttributes(mods, starRating)
{
AimStrain = aimRating,
SpeedStrain = speedRating
SpeedStrain = speedRating,
ApproachRate = preEmpt > 1200 ? (1800 - preEmpt) / 120 : (1200 - preEmpt) / 150 + 5,
OverallDifficulty = (80 - hitWindowGreat) / 6,
MaxCombo = maxCombo
};
}

View File

@ -22,16 +22,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
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 countGreat;
@ -63,13 +53,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (mods.Any(m => !m.Ranked))
return 0;
// Todo: These int casts are temporary to achieve 1:1 results with osu!stable, and should be remoevd in the future
double hitWindowGreat = (int)(Beatmap.HitObjects.First().HitWindows.Great / 2) / TimeRate;
double preEmpt = (int)BeatmapDifficulty.DifficultyRange(Beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / TimeRate;
realApproachRate = preEmpt > 1200 ? (1800 - preEmpt) / 120 : (1200 - preEmpt) / 150 + 5;
realOverallDifficulty = (80 - 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
@ -94,8 +77,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
categoryRatings.Add("Aim", aimValue);
categoryRatings.Add("Speed", speedValue);
categoryRatings.Add("Accuracy", accuracyValue);
categoryRatings.Add("OD", realOverallDifficulty);
categoryRatings.Add("AR", realApproachRate);
categoryRatings.Add("OD", Attributes.OverallDifficulty);
categoryRatings.Add("AR", Attributes.ApproachRate);
categoryRatings.Add("Max Combo", beatmapMaxCombo);
}
@ -120,22 +103,22 @@ namespace osu.Game.Rulesets.Osu.Difficulty
aimValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8f) / Math.Pow(beatmapMaxCombo, 0.8f), 1.0f);
double approachRateFactor = 1.0f;
if (realApproachRate > 10.33f)
approachRateFactor += 0.45f * (realApproachRate - 10.33f);
else if (realApproachRate < 8.0f)
if (Attributes.ApproachRate > 10.33f)
approachRateFactor += 0.45f * (Attributes.ApproachRate - 10.33f);
else if (Attributes.ApproachRate < 8.0f)
{
// HD is worth more with lower ar!
if (mods.Any(h => h is OsuModHidden))
approachRateFactor += 0.02f * (8.0f - realApproachRate);
approachRateFactor += 0.02f * (8.0f - Attributes.ApproachRate);
else
approachRateFactor += 0.01f * (8.0f - realApproachRate);
approachRateFactor += 0.01f * (8.0f - Attributes.ApproachRate);
}
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.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.
aimValue *= 1.02 + (11.0f - Attributes.ApproachRate) / 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))
{
@ -146,7 +129,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
// 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(realOverallDifficulty, 2) / 2500;
aimValue *= 0.98f + Math.Pow(Attributes.OverallDifficulty, 2) / 2500;
return aimValue;
}
@ -172,7 +155,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
// 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(realOverallDifficulty, 2) / 2500;
speedValue *= 0.98f + Math.Pow(Attributes.OverallDifficulty, 2) / 2500;
return speedValue;
}
@ -194,7 +177,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
// 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, realOverallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83f;
double accuracyValue = Math.Pow(1.52163f, Attributes.OverallDifficulty) * 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));

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 OpenTK;
using osu.Game.Rulesets.Objects.Types;
using System.Collections.Generic;
@ -45,6 +46,8 @@ namespace osu.Game.Rulesets.Osu.Objects
set { Curve.Distance = value; }
}
public double? LegacyLastTickOffset { get; set; }
/// <summary>
/// The position of the cursor at the point of completion of this <see cref="Slider"/> if it was hit
/// with as few movements as possible. This is set and used by difficulty calculation.
@ -91,6 +94,9 @@ namespace osu.Game.Rulesets.Osu.Objects
createSliderEnds();
createTicks();
createRepeatPoints();
if (LegacyLastTickOffset != null)
TailCircle.StartTime = Math.Max(StartTime + Duration / 2, TailCircle.StartTime - LegacyLastTickOffset.Value);
}
private void createSliderEnds()

View File

@ -6,6 +6,7 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using System;
using System.Collections.Generic;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Replays;
using osu.Game.Users;
@ -18,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Replays
/// <summary>
/// Constants (for spinners).
/// </summary>
protected static readonly Vector2 SPINNER_CENTRE = new Vector2(256, 192);
protected static readonly Vector2 SPINNER_CENTRE = OsuPlayfield.BASE_SIZE / 2;
protected const float SPIN_RADIUS = 50;
/// <summary>

View File

@ -30,13 +30,16 @@ namespace osu.Game.Rulesets.Osu.Replays
}
}
public override List<InputState> GetPendingStates()
public override List<IInput> GetPendingInputs()
{
return new List<InputState>
return new List<IInput>
{
new MousePositionAbsoluteInput
{
Position = GamefieldToScreenSpace(Position ?? Vector2.Zero)
},
new ReplayState<OsuAction>
{
Mouse = new ReplayMouseState(GamefieldToScreenSpace(Position ?? Vector2.Zero)),
PressedActions = CurrentFrame.Actions
}
};

View File

@ -1,124 +1,256 @@
{
"Mappings": [{
"StartTime": 500,
"Objects": [{
"StartTime": 500,
"EndTime": 2500,
"StartX": 96,
"StartY": 192,
"EndX": 96,
"EndY": 192
}]
},
{
"StartTime": 3000,
"Objects": [{
"StartTime": 3000,
"EndTime": 4000,
"StartX": 256,
"StartY": 192,
"EndX": 256,
"EndY": 192
}]
},
{
"StartTime": 4500,
"Objects": [{
"StartTime": 4500,
"EndTime": 5500,
"StartX": 256,
"StartY": 192,
"EndX": 256,
"EndY": 192
}]
},
{
"StartTime": 6000,
"Objects": [{
"StartTime": 6000,
"EndTime": 6500,
"StartX": 256,
"StartY": 192,
"EndX": 256,
"EndY": 192
}]
},
{
"StartTime": 7000,
"Objects": [{
"StartTime": 7000,
"EndTime": 8000,
"StartX": 256,
"StartY": 128,
"EndX": 256,
"EndY": 128
}]
},
{
"StartTime": 8500,
"Objects": [{
"StartTime": 8500,
"EndTime": 10999,
"StartX": 32,
"StartY": 192,
"EndX": 508.166229,
"EndY": 153.299271
}]
},
{
"StartTime": 11500,
"Objects": [{
"StartTime": 11500,
"EndTime": 12000,
"StartX": 256,
"StartY": 192,
"EndX": 256,
"EndY": 192
}]
},
{
"StartTime": 12500,
"Objects": [{
"StartTime": 12500,
"EndTime": 16500,
"StartX": 512,
"StartY": 320,
"EndX": 291.1977,
"EndY": 40.799427
}]
},
{
"StartTime": 17000,
"Objects": [{
"StartTime": 17000,
"EndTime": 18000,
"StartX": 256,
"StartY": 256,
"EndX": 256,
"EndY": 256
}]
},
{
"StartTime": 18500,
"Objects": [{
"StartTime": 18500,
"EndTime": 19450,
"StartX": 256,
"StartY": 192,
"EndX": 256,
"EndY": 192
}]
},
{
"StartTime": 19875,
"Objects": [{
"StartTime": 19875,
"EndTime": 23874,
"StartX": 216,
"StartY": 231,
"EndX": 408.720825,
"EndY": 339.810455
}]
}
]
"StartTime": 500.0,
"Objects": [{
"StartTime": 500.0,
"EndTime": 500.0,
"X": 96.0,
"Y": 192.0
}, {
"StartTime": 1000.0,
"EndTime": 1000.0,
"X": 256.0,
"Y": 192.0
}, {
"StartTime": 1500.0,
"EndTime": 1500.0,
"X": 416.0,
"Y": 192.0
}, {
"StartTime": 2000.0,
"EndTime": 2000.0,
"X": 256.0,
"Y": 192.0
}, {
"StartTime": 2464.0,
"EndTime": 2464.0,
"X": 96.0,
"Y": 192.0
}]
}, {
"StartTime": 3000.0,
"Objects": [{
"StartTime": 3000.0,
"EndTime": 4000.0,
"X": 256.0,
"Y": 192.0
}]
}, {
"StartTime": 4500.0,
"Objects": [{
"StartTime": 4500.0,
"EndTime": 5500.0,
"X": 256.0,
"Y": 192.0
}]
}, {
"StartTime": 6000.0,
"Objects": [{
"StartTime": 6000.0,
"EndTime": 6500.0,
"X": 256.0,
"Y": 192.0
}]
}, {
"StartTime": 7000.0,
"Objects": [{
"StartTime": 7000.0,
"EndTime": 7000.0,
"X": 256.0,
"Y": 128.0
}, {
"StartTime": 7250.0,
"EndTime": 7250.0,
"X": 336.0,
"Y": 128.0
}, {
"StartTime": 7500.0,
"EndTime": 7500.0,
"X": 256.0,
"Y": 128.0
}, {
"StartTime": 7750.0,
"EndTime": 7750.0,
"X": 336.0,
"Y": 128.0
}, {
"StartTime": 7964.0,
"EndTime": 7964.0,
"X": 256.0,
"Y": 128.0
}]
}, {
"StartTime": 8500.0,
"Objects": [{
"StartTime": 8500.0,
"EndTime": 8500.0,
"X": 32.0,
"Y": 192.0
}, {
"StartTime": 9000.0,
"EndTime": 9000.0,
"X": 101.81015,
"Y": 326.4915
}, {
"StartTime": 9500.0,
"EndTime": 9500.0,
"X": 237.2304,
"Y": 276.282928
}, {
"StartTime": 10000.0,
"EndTime": 10000.0,
"X": 270.339874,
"Y": 121.1423
}, {
"StartTime": 10500.0,
"EndTime": 10500.0,
"X": 401.0588,
"Y": 49.1515045
}, {
"StartTime": 10964.0,
"EndTime": 10964.0,
"X": 508.166229,
"Y": 153.299271
}]
}, {
"StartTime": 11500.0,
"Objects": [{
"StartTime": 11500.0,
"EndTime": 12000.0,
"X": 256.0,
"Y": 192.0
}]
}, {
"StartTime": 12500.0,
"Objects": [{
"StartTime": 12500.0,
"EndTime": 12500.0,
"X": 512.0,
"Y": 320.0
}, {
"StartTime": 13000.0,
"EndTime": 13000.0,
"X": 353.235535,
"Y": 300.154449
}, {
"StartTime": 13500.0,
"EndTime": 13500.0,
"X": 194.471069,
"Y": 280.3089
}, {
"StartTime": 14000.0,
"EndTime": 14000.0,
"X": 35.7066345,
"Y": 260.463318
}, {
"StartTime": 14500.0,
"EndTime": 14500.0,
"X": 118.370323,
"Y": 219.009277
}, {
"StartTime": 15000.0,
"EndTime": 15000.0,
"X": 271.087128,
"Y": 171.285278
}, {
"StartTime": 15500.0,
"EndTime": 15500.0,
"X": 423.803925,
"Y": 123.561279
}, {
"StartTime": 16000.0,
"EndTime": 16000.0,
"X": 446.420532,
"Y": 79.60513
}, {
"StartTime": 16464.0,
"EndTime": 16464.0,
"X": 291.1977,
"Y": 40.799427
}]
}, {
"StartTime": 17000.0,
"Objects": [{
"StartTime": 17000.0,
"EndTime": 17000.0,
"X": 256.0,
"Y": 256.0
}, {
"StartTime": 17250.0,
"EndTime": 17250.0,
"X": 176.0,
"Y": 256.0
}, {
"StartTime": 17500.0,
"EndTime": 17500.0,
"X": 256.0,
"Y": 256.0
}, {
"StartTime": 17750.0,
"EndTime": 17750.0,
"X": 176.0,
"Y": 256.0
}, {
"StartTime": 17964.0,
"EndTime": 17964.0,
"X": 256.0,
"Y": 256.0
}]
}, {
"StartTime": 18500.0,
"Objects": [{
"StartTime": 18500.0,
"EndTime": 19450.0,
"X": 256.0,
"Y": 192.0
}]
}, {
"StartTime": 19875.0,
"Objects": [{
"StartTime": 19875.0,
"EndTime": 19875.0,
"X": 216.0,
"Y": 231.0
}, {
"StartTime": 20375.0,
"EndTime": 20375.0,
"X": 317.446747,
"Y": 171.345245
}, {
"StartTime": 20875.0,
"EndTime": 20875.0,
"X": 270.3294,
"Y": 310.4395
}, {
"StartTime": 21375.0,
"EndTime": 21375.0,
"X": 119.121056,
"Y": 322.8657
}, {
"StartTime": 21875.0,
"EndTime": 21875.0,
"X": 124.28746,
"Y": 165.224731
}, {
"StartTime": 22375.0,
"EndTime": 22375.0,
"X": 240.4715,
"Y": 62.65587
}, {
"StartTime": 22875.0,
"EndTime": 22875.0,
"X": 398.054047,
"Y": 39.064167
}, {
"StartTime": 23375.0,
"EndTime": 23375.0,
"X": 439.749878,
"Y": 183.668091
}, {
"StartTime": 23839.0,
"EndTime": 23839.0,
"X": 408.720825,
"Y": 339.810455
}]
}]
}

View File

@ -1,13 +1,16 @@
{
"Mappings": [{
"StartTime": 118858,
"Objects": [{
"StartTime": 118858,
"EndTime": 119088,
"StartX": 219,
"StartY": 215,
"EndX": 239.6507,
"EndY": 29.1437378
"Mappings": [{
"StartTime": 118858.0,
"Objects": [{
"StartTime": 118858.0,
"EndTime": 118858.0,
"X": 219.0,
"Y": 215.0
}, {
"StartTime": 119052.0,
"EndTime": 119052.0,
"X": 239.6507,
"Y": 29.1437378
}]
}]
}]
}

View File

@ -0,0 +1,331 @@
{
"Mappings": [{
"StartTime": 500.0,
"Objects": [{
"StartTime": 500.0,
"EndTime": 500.0,
"X": 96.0,
"Y": 192.0
}, {
"StartTime": 624.0,
"EndTime": 624.0,
"X": 105.921242,
"Y": 192.0
}, {
"StartTime": 749.0,
"EndTime": 749.0,
"X": 115.922493,
"Y": 192.0
}, {
"StartTime": 874.0,
"EndTime": 874.0,
"X": 125.923737,
"Y": 192.0
}, {
"StartTime": 999.0,
"EndTime": 999.0,
"X": 135.924988,
"Y": 192.0
}, {
"StartTime": 1124.0,
"EndTime": 1124.0,
"X": 145.926239,
"Y": 192.0
}, {
"StartTime": 1249.0,
"EndTime": 1249.0,
"X": 155.92749,
"Y": 192.0
}, {
"StartTime": 1374.0,
"EndTime": 1374.0,
"X": 165.928741,
"Y": 192.0
}, {
"StartTime": 1499.0,
"EndTime": 1499.0,
"X": 175.93,
"Y": 192.0
}, {
"StartTime": 1624.0,
"EndTime": 1624.0,
"X": 185.931244,
"Y": 192.0
}, {
"StartTime": 1749.0,
"EndTime": 1749.0,
"X": 195.9325,
"Y": 192.0
}, {
"StartTime": 1874.0,
"EndTime": 1874.0,
"X": 205.933746,
"Y": 192.0
}, {
"StartTime": 1999.0,
"EndTime": 1999.0,
"X": 215.935,
"Y": 192.0
}, {
"StartTime": 2124.0,
"EndTime": 2124.0,
"X": 225.936234,
"Y": 192.0
}, {
"StartTime": 2249.0,
"EndTime": 2249.0,
"X": 235.9375,
"Y": 192.0
}, {
"StartTime": 2374.0,
"EndTime": 2374.0,
"X": 245.938751,
"Y": 192.0
}, {
"StartTime": 2499.0,
"EndTime": 2499.0,
"X": 255.94,
"Y": 192.0
}, {
"StartTime": 2624.0,
"EndTime": 2624.0,
"X": 265.941223,
"Y": 192.0
}, {
"StartTime": 2749.0,
"EndTime": 2749.0,
"X": 275.9425,
"Y": 192.0
}, {
"StartTime": 2874.0,
"EndTime": 2874.0,
"X": 285.943756,
"Y": 192.0
}, {
"StartTime": 2999.0,
"EndTime": 2999.0,
"X": 295.945,
"Y": 192.0
}, {
"StartTime": 3124.0,
"EndTime": 3124.0,
"X": 305.946259,
"Y": 192.0
}, {
"StartTime": 3249.0,
"EndTime": 3249.0,
"X": 315.9475,
"Y": 192.0
}, {
"StartTime": 3374.0,
"EndTime": 3374.0,
"X": 325.94873,
"Y": 192.0
}, {
"StartTime": 3499.0,
"EndTime": 3499.0,
"X": 335.949982,
"Y": 192.0
}, {
"StartTime": 3624.0,
"EndTime": 3624.0,
"X": 345.951233,
"Y": 192.0
}, {
"StartTime": 3749.0,
"EndTime": 3749.0,
"X": 355.952484,
"Y": 192.0
}, {
"StartTime": 3874.0,
"EndTime": 3874.0,
"X": 365.953766,
"Y": 192.0
}, {
"StartTime": 3999.0,
"EndTime": 3999.0,
"X": 375.955,
"Y": 192.0
}, {
"StartTime": 4124.0,
"EndTime": 4124.0,
"X": 385.956238,
"Y": 192.0
}, {
"StartTime": 4249.0,
"EndTime": 4249.0,
"X": 395.9575,
"Y": 192.0
}, {
"StartTime": 4374.0,
"EndTime": 4374.0,
"X": 405.95874,
"Y": 192.0
}, {
"StartTime": 4499.0,
"EndTime": 4499.0,
"X": 415.960022,
"Y": 192.0
}, {
"StartTime": 4624.0,
"EndTime": 4624.0,
"X": 406.038757,
"Y": 192.0
}, {
"StartTime": 4749.0,
"EndTime": 4749.0,
"X": 396.0375,
"Y": 192.0
}, {
"StartTime": 4874.0,
"EndTime": 4874.0,
"X": 386.036255,
"Y": 192.0
}, {
"StartTime": 4999.0,
"EndTime": 4999.0,
"X": 376.035034,
"Y": 192.0
}, {
"StartTime": 5124.0,
"EndTime": 5124.0,
"X": 366.033752,
"Y": 192.0
}, {
"StartTime": 5249.0,
"EndTime": 5249.0,
"X": 356.0325,
"Y": 192.0
}, {
"StartTime": 5374.0,
"EndTime": 5374.0,
"X": 346.03125,
"Y": 192.0
}, {
"StartTime": 5499.0,
"EndTime": 5499.0,
"X": 336.030029,
"Y": 192.0
}, {
"StartTime": 5624.0,
"EndTime": 5624.0,
"X": 326.028748,
"Y": 192.0
}, {
"StartTime": 5749.0,
"EndTime": 5749.0,
"X": 316.0275,
"Y": 192.0
}, {
"StartTime": 5874.0,
"EndTime": 5874.0,
"X": 306.026245,
"Y": 192.0
}, {
"StartTime": 5999.0,
"EndTime": 5999.0,
"X": 296.025,
"Y": 192.0
}, {
"StartTime": 6124.0,
"EndTime": 6124.0,
"X": 286.023773,
"Y": 192.0
}, {
"StartTime": 6249.0,
"EndTime": 6249.0,
"X": 276.022522,
"Y": 192.0
}, {
"StartTime": 6374.0,
"EndTime": 6374.0,
"X": 266.02124,
"Y": 192.0
}, {
"StartTime": 6499.0,
"EndTime": 6499.0,
"X": 256.02,
"Y": 192.0
}, {
"StartTime": 6624.0,
"EndTime": 6624.0,
"X": 246.018768,
"Y": 192.0
}, {
"StartTime": 6749.0,
"EndTime": 6749.0,
"X": 236.017517,
"Y": 192.0
}, {
"StartTime": 6874.0,
"EndTime": 6874.0,
"X": 226.016251,
"Y": 192.0
}, {
"StartTime": 6999.0,
"EndTime": 6999.0,
"X": 216.014984,
"Y": 192.0
}, {
"StartTime": 7124.0,
"EndTime": 7124.0,
"X": 206.013733,
"Y": 192.0
}, {
"StartTime": 7249.0,
"EndTime": 7249.0,
"X": 196.012512,
"Y": 192.0
}, {
"StartTime": 7374.0,
"EndTime": 7374.0,
"X": 186.011261,
"Y": 192.0
}, {
"StartTime": 7499.0,
"EndTime": 7499.0,
"X": 176.01,
"Y": 192.0
}, {
"StartTime": 7624.0,
"EndTime": 7624.0,
"X": 166.008728,
"Y": 192.0
}, {
"StartTime": 7749.0,
"EndTime": 7749.0,
"X": 156.0075,
"Y": 192.0
}, {
"StartTime": 7874.0,
"EndTime": 7874.0,
"X": 146.006256,
"Y": 192.0
}, {
"StartTime": 7999.0,
"EndTime": 7999.0,
"X": 136.005,
"Y": 192.0
}, {
"StartTime": 8124.0,
"EndTime": 8124.0,
"X": 126.003738,
"Y": 192.0
}, {
"StartTime": 8249.0,
"EndTime": 8249.0,
"X": 116.002518,
"Y": 192.0
}, {
"StartTime": 8374.0,
"EndTime": 8374.0,
"X": 106.001259,
"Y": 192.0
}, {
"StartTime": 8463.0,
"EndTime": 8463.0,
"X": 96.0,
"Y": 192.0
}]
}]
}

View File

@ -0,0 +1,20 @@
osu file format v14
[General]
StackLeniency: 0.7
[Difficulty]
HPDrainRate:6
CircleSize:4
OverallDifficulty:7
ApproachRate:8.3
SliderMultiplier:0.400000005960464
SliderTickRate:4
[TimingPoints]
500,500,4,2,1,50,1,0
13426,-100,4,3,1,45,0,0
14884,-100,4,2,1,50,0,0
[HitObjects]
96,192,500,6,0,L|416:192,2,320.000004768372

View File

@ -12,7 +12,8 @@ using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Taiko.Tests
{
internal class TaikoBeatmapConversionTest : BeatmapConversionTest<ConvertValue>
[TestFixture]
public class TaikoBeatmapConversionTest : BeatmapConversionTest<ConvertValue>
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
@ -41,7 +42,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
protected override Ruleset CreateRuleset() => new TaikoRuleset();
}
internal struct ConvertValue : IEquatable<ConvertValue>
public struct ConvertValue : IEquatable<ConvertValue>
{
/// <summary>
/// A sane value to account for osu!stable using ints everwhere.

View File

@ -0,0 +1,19 @@
// 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.Difficulty;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Taiko.Difficulty
{
public class TaikoDifficultyAttributes : DifficultyAttributes
{
public double GreatHitWindow;
public int MaxCombo;
public TaikoDifficultyAttributes(Mod[] mods, double starRating)
: base(mods, starRating)
{
}
}
}

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
@ -34,6 +35,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double timeRate)
{
if (!beatmap.HitObjects.Any())
return new TaikoDifficultyAttributes(mods, 0);
var difficultyHitObjects = new List<TaikoHitObjectDifficulty>();
foreach (var hitObject in beatmap.HitObjects)
@ -47,7 +51,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
double starRating = calculateDifficulty(difficultyHitObjects, timeRate) * star_scaling_factor;
return new DifficultyAttributes(mods, starRating);
return new TaikoDifficultyAttributes(mods, starRating)
{
// Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be remoevd in the future
GreatHitWindow = (int)(beatmap.HitObjects.First().HitWindows.Great / 2) / timeRate,
MaxCombo = beatmap.HitObjects.Count(h => h is Hit)
};
}
private bool calculateStrainValues(List<TaikoHitObjectDifficulty> objects, double timeRate)

View File

@ -8,13 +8,12 @@ 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;
protected new TaikoDifficultyAttributes Attributes => (TaikoDifficultyAttributes)base.Attributes;
private Mod[] mods;
private int countGreat;
@ -25,7 +24,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
public TaikoPerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, Score score)
: base(ruleset, beatmap, score)
{
beatmapMaxCombo = Beatmap.HitObjects.Count(h => h is Hit);
}
public override double Calculate(Dictionary<string, double> categoryDifficulty = null)
@ -78,8 +76,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
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 (Attributes.MaxCombo > 0)
strainValue *= Math.Min(Math.Pow(Score.MaxCombo, 0.5) / Math.Pow(Attributes.MaxCombo, 0.5), 1.0);
if (mods.Any(m => m is ModHidden))
strainValue *= 1.025;
@ -94,14 +92,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
private double computeAccuracyValue()
{
// Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be remoevd in the future
double hitWindowGreat = (int)(Beatmap.HitObjects.First().HitWindows.Great / 2) / TimeRate;
if (hitWindowGreat <= 0)
if (Attributes.GreatHitWindow <= 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;
double accValue = Math.Pow(150.0 / Attributes.GreatHitWindow, 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));

View File

@ -0,0 +1,26 @@
// 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.Taiko.Judgements
{
public class TaikoIntermediateSwellJudgement : TaikoJudgement
{
public override HitResult MaxResult => HitResult.Perfect;
public override bool AffectsCombo => false;
public TaikoIntermediateSwellJudgement()
{
Final = false;
}
/// <summary>
/// Computes the numeric result value for the combo portion of the score.
/// </summary>
/// <param name="result">The result to compute the value for.</param>
/// <returns>The numeric result value.</returns>
protected override int NumericResultFor(HitResult result) => 0;
}
}

View File

@ -86,6 +86,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
switch (State.Value)
{
case ArmedState.Idle:
SecondHitAllowed = false;
validKeyPressed = false;
UnproxyContent();
this.Delay(HitObject.HitWindows.HalfWindowFor(HitResult.Miss)).Expire();
break;
@ -95,8 +98,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
break;
case ArmedState.Hit:
// If we're far enough away from the left stage, we should bring outselves in front of it
if (X >= -0.05f)
ProxyContent();
ProxyContent();
var flash = circlePiece?.FlashBox;
if (flash != null)

View File

@ -3,6 +3,7 @@
using System;
using System.Linq;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Judgements;
@ -46,6 +47,19 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
AddJudgement(new TaikoStrongHitJudgement { Result = HitResult.Great });
}
protected override void UpdateState(ArmedState state)
{
base.UpdateState(state);
switch (state)
{
case ArmedState.Idle:
firstHitTime = 0;
firstKeyHeld = false;
break;
}
}
public override bool OnReleased(TaikoAction action)
{
if (action == firstHitAction)

View File

@ -2,7 +2,6 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
@ -20,6 +19,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
public class DrawableSwell : DrawableTaikoHitObject<Swell>
{
/// <summary>
/// A judgement is only displayed when the user has complete the swell (either a hit or miss).
/// </summary>
public override bool DisplayJudgement => AllJudged;
private const float target_ring_thick_border = 1.4f;
private const float target_ring_thin_border = 1f;
private const float target_ring_scale = 5f;
@ -29,11 +33,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
private readonly CircularContainer targetRing;
private readonly CircularContainer expandingRing;
/// <summary>
/// The amount of times the user has hit this swell.
/// </summary>
private int userHits;
private readonly SwellSymbolPiece symbol;
public DrawableSwell(Swell swell)
@ -129,9 +128,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
if (userTriggered)
{
userHits++;
AddJudgement(new TaikoIntermediateSwellJudgement());
var completion = (float)userHits / HitObject.RequiredHits;
var completion = (float)Judgements.Count / HitObject.RequiredHits;
expandingRing
.FadeTo(expandingRing.Alpha + MathHelper.Clamp(completion / 16, 0.1f, 0.6f), 50)
@ -142,7 +141,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, Easing.OutQuint);
if (userHits == HitObject.RequiredHits)
if (Judgements.Count == HitObject.RequiredHits)
AddJudgement(new TaikoJudgement { Result = HitResult.Great });
}
else
@ -151,7 +150,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
return;
//TODO: THIS IS SHIT AND CAN'T EXIST POST-TAIKO WORLD CUP
AddJudgement(userHits > HitObject.RequiredHits / 2
AddJudgement(Judgements.Count > HitObject.RequiredHits / 2
? new TaikoJudgement { Result = HitResult.Good }
: new TaikoJudgement { Result = HitResult.Miss });
}
@ -162,23 +161,22 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
const float preempt = 100;
const float out_transition_time = 300;
double untilStartTime = HitObject.StartTime - Time.Current;
double untilJudgement = untilStartTime + (Judgements.FirstOrDefault()?.TimeOffset ?? 0) + HitObject.Duration;
targetRing.Delay(untilStartTime - preempt).ScaleTo(target_ring_scale, preempt * 4, Easing.OutQuint);
this.Delay(untilJudgement).FadeOut(out_transition_time, Easing.Out);
switch (state)
{
case ArmedState.Idle:
UnproxyContent();
expandingRing.FadeTo(0);
using (BeginAbsoluteSequence(HitObject.StartTime - preempt, true))
targetRing.ScaleTo(target_ring_scale, preempt * 4, Easing.OutQuint);
break;
case ArmedState.Miss:
case ArmedState.Hit:
bodyContainer.Delay(untilJudgement).ScaleTo(1.4f, out_transition_time);
this.FadeOut(out_transition_time, Easing.Out);
bodyContainer.ScaleTo(1.4f, out_transition_time);
Expire();
break;
}
Expire();
}
protected override void Update()

View File

@ -17,6 +17,6 @@ namespace osu.Game.Rulesets.Taiko.Replays
protected override bool IsImportant(TaikoReplayFrame frame) => frame.Actions.Any();
public override List<InputState> GetPendingStates() => new List<InputState> { new ReplayState<TaikoAction> { PressedActions = CurrentFrame.Actions } };
public override List<IInput> GetPendingInputs() => new List<IInput> { new ReplayState<TaikoAction> { PressedActions = CurrentFrame.Actions } };
}
}

View File

@ -27,14 +27,12 @@ namespace osu.Game.Rulesets.Taiko
public override IEnumerable<KeyBinding> GetDefaultKeyBindings(int variant = 0) => new[]
{
new KeyBinding(InputKey.MouseLeft, TaikoAction.LeftCentre),
new KeyBinding(InputKey.MouseRight, TaikoAction.LeftRim),
new KeyBinding(InputKey.D, TaikoAction.LeftRim),
new KeyBinding(InputKey.F, TaikoAction.LeftCentre),
new KeyBinding(InputKey.J, TaikoAction.RightCentre),
new KeyBinding(InputKey.K, TaikoAction.RightRim),
new KeyBinding(InputKey.MouseLeft, TaikoAction.LeftCentre),
new KeyBinding(InputKey.MouseLeft, TaikoAction.RightCentre),
new KeyBinding(InputKey.MouseRight, TaikoAction.LeftRim),
new KeyBinding(InputKey.MouseRight, TaikoAction.RightRim),
};
public override IEnumerable<Mod> ConvertLegacyMods(LegacyMods mods)

View File

@ -69,11 +69,7 @@ namespace osu.Game.Rulesets.Taiko.UI
bool isMajor = currentBeat % (int)currentPoint.TimeSignature == 0;
Playfield.Add(isMajor ? new DrawableBarLineMajor(barLine) : new DrawableBarLine(barLine));
double bl = currentPoint.BeatLength;
if (bl < 800)
bl *= (int)currentPoint.TimeSignature;
time += bl;
time += currentPoint.BeatLength * (int)currentPoint.TimeSignature;
currentBeat++;
}
}

View File

@ -1,8 +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.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
@ -10,9 +9,6 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Beatmaps;
using OpenTK;
@ -20,10 +16,9 @@ using OpenTK.Graphics;
namespace osu.Game.Tests.Visual
{
[TestFixture]
public class TestCaseEditorSeekSnapping : EditorClockTestCase
{
public override IReadOnlyList<Type> RequiredTypes => new[] { typeof(HitObjectComposer) };
public TestCaseEditorSeekSnapping()
{
BeatDivisor.Value = 4;
@ -56,22 +51,13 @@ namespace osu.Game.Tests.Visual
Beatmap.Value = new TestWorkingBeatmap(testBeatmap);
Child = new TimingPointVisualiser(testBeatmap, 5000) { Clock = Clock };
testSeekNoSnapping();
testSeekSnappingOnBeat();
testSeekSnappingInBetweenBeat();
testSeekForwardNoSnapping();
testSeekForwardSnappingOnBeat();
testSeekForwardSnappingFromInBetweenBeat();
testSeekBackwardSnappingOnBeat();
testSeekBackwardSnappingFromInBetweenBeat();
testSeekingWithFloatingPointBeatLength();
}
/// <summary>
/// Tests whether time is correctly seeked without snapping.
/// </summary>
private void testSeekNoSnapping()
[Test]
public void TestSeekNoSnapping()
{
reset();
@ -94,7 +80,8 @@ namespace osu.Game.Tests.Visual
/// Tests whether seeking to exact beat times puts us on the beat time.
/// These are the white/yellow ticks on the graph.
/// </summary>
private void testSeekSnappingOnBeat()
[Test]
public void TestSeekSnappingOnBeat()
{
reset();
@ -117,9 +104,9 @@ namespace osu.Game.Tests.Visual
/// <summary>
/// Tests whether seeking to somewhere in the middle between beats puts us on the expected beats.
/// For example, snapping between a white/yellow beat should put us on either the yellow or white, depending on which one we're closer too.
/// If
/// </summary>
private void testSeekSnappingInBetweenBeat()
[Test]
public void TestSeekSnappingInBetweenBeat()
{
reset();
@ -140,7 +127,8 @@ namespace osu.Game.Tests.Visual
/// <summary>
/// Tests that when seeking forward with no beat snapping, beats are never explicitly snapped to, nor the next timing point (if we've skipped it).
/// </summary>
private void testSeekForwardNoSnapping()
[Test]
public void TestSeekForwardNoSnapping()
{
reset();
@ -159,7 +147,8 @@ namespace osu.Game.Tests.Visual
/// <summary>
/// Tests that when seeking forward with beat snapping, all beats are snapped to and timing points are never skipped.
/// </summary>
private void testSeekForwardSnappingOnBeat()
[Test]
public void TestSeekForwardSnappingOnBeat()
{
reset();
@ -181,7 +170,8 @@ namespace osu.Game.Tests.Visual
/// Tests that when seeking forward from in-between two beats, the next beat or timing point is snapped to, and no beats are skipped.
/// This will also test being extremely close to the next beat/timing point, to ensure rounding is not an issue.
/// </summary>
private void testSeekForwardSnappingFromInBetweenBeat()
[Test]
public void TestSeekForwardSnappingFromInBetweenBeat()
{
reset();
@ -214,21 +204,20 @@ namespace osu.Game.Tests.Visual
/// <summary>
/// Tests that when seeking backward with no beat snapping, beats are never explicitly snapped to, nor the next timing point (if we've skipped it).
/// </summary>
private void testSeekBackwardNoSnapping()
[Test]
public void TestSeekBackwardNoSnapping()
{
reset();
AddStep("Seek(450)", () => Clock.Seek(450));
AddStep("SeekBackward", () => Clock.SeekBackward());
AddAssert("Time = 425", () => Clock.CurrentTime == 425);
AddAssert("Time = 400", () => Clock.CurrentTime == 400);
AddStep("SeekBackward", () => Clock.SeekBackward());
AddAssert("Time = 375", () => Clock.CurrentTime == 375);
AddAssert("Time = 350", () => Clock.CurrentTime == 350);
AddStep("SeekBackward", () => Clock.SeekBackward());
AddAssert("Time = 325", () => Clock.CurrentTime == 325);
AddAssert("Time = 150", () => Clock.CurrentTime == 150);
AddStep("SeekBackward", () => Clock.SeekBackward());
AddAssert("Time = 125", () => Clock.CurrentTime == 125);
AddStep("SeekBackward", () => Clock.SeekBackward());
AddAssert("Time = 25", () => Clock.CurrentTime == 25);
AddAssert("Time = 50", () => Clock.CurrentTime == 50);
AddStep("SeekBackward", () => Clock.SeekBackward());
AddAssert("Time = 0", () => Clock.CurrentTime == 0);
}
@ -236,7 +225,8 @@ namespace osu.Game.Tests.Visual
/// <summary>
/// Tests that when seeking backward with beat snapping, all beats are snapped to and timing points are never skipped.
/// </summary>
private void testSeekBackwardSnappingOnBeat()
[Test]
public void TestSeekBackwardSnappingOnBeat()
{
reset();
@ -259,7 +249,8 @@ namespace osu.Game.Tests.Visual
/// Tests that when seeking backward from in-between two beats, the previous beat or timing point is snapped to, and no beats are skipped.
/// This will also test being extremely close to the previous beat/timing point, to ensure rounding is not an issue.
/// </summary>
private void testSeekBackwardSnappingFromInBetweenBeat()
[Test]
public void TestSeekBackwardSnappingFromInBetweenBeat()
{
reset();
@ -280,7 +271,8 @@ namespace osu.Game.Tests.Visual
/// <summary>
/// Tests that there are no rounding issues when snapping to beats within a timing point with a floating-point beatlength.
/// </summary>
private void testSeekingWithFloatingPointBeatLength()
[Test]
public void TestSeekingWithFloatingPointBeatLength()
{
reset();
@ -288,7 +280,7 @@ namespace osu.Game.Tests.Visual
AddStep("Seek(0)", () => Clock.Seek(0));
for (int i = 0; i < 20; i++)
for (int i = 0; i < 9; i++)
{
AddStep("SeekForward, Snap", () =>
{
@ -298,7 +290,7 @@ namespace osu.Game.Tests.Visual
AddAssert("Time > lastTime", () => Clock.CurrentTime > lastTime);
}
for (int i = 0; i < 20; i++)
for (int i = 0; i < 9; i++)
{
AddStep("SeekBackward, Snap", () =>
{
@ -316,16 +308,6 @@ namespace osu.Game.Tests.Visual
AddStep("Reset", () => Clock.Seek(0));
}
private class TestHitObjectComposer : HitObjectComposer
{
public TestHitObjectComposer(Ruleset ruleset)
: base(ruleset)
{
}
protected override IReadOnlyList<ICompositionTool> CompositionTools => new ICompositionTool[0];
}
private class TimingPointVisualiser : CompositeDrawable
{
private readonly double length;

View File

@ -0,0 +1,63 @@
// 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.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
using OpenTK.Graphics;
namespace osu.Game.Tests.Visual
{
public class TestCaseLoadingAnimation : GridTestCase
{
public TestCaseLoadingAnimation()
: base(2, 2)
{
LoadingAnimation loading;
Cell(0).AddRange(new Drawable[]
{
new Box
{
Colour = Color4.Black,
RelativeSizeAxes = Axes.Both
},
loading = new LoadingAnimation()
});
loading.Show();
Cell(1).AddRange(new Drawable[]
{
new Box
{
Colour = Color4.White,
RelativeSizeAxes = Axes.Both
},
loading = new LoadingAnimation()
});
loading.Show();
Cell(2).AddRange(new Drawable[]
{
new Box
{
Colour = Color4.Gray,
RelativeSizeAxes = Axes.Both
},
loading = new LoadingAnimation()
});
loading.Show();
Cell(3).AddRange(new Drawable[]
{
loading = new LoadingAnimation()
});
Scheduler.AddDelayed(() => loading.ToggleVisibility(), 200, true);
}
}
}

View File

@ -8,6 +8,8 @@ using osu.Framework.Allocation;
using osu.Game.Beatmaps;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets;
using osu.Game.Screens;
using osu.Game.Screens.Backgrounds;
using osu.Game.Screens.Multi.Components;
using osu.Game.Screens.Multi.Screens.Lounge;
using osu.Game.Users;
@ -165,6 +167,7 @@ namespace osu.Game.Tests.Visual
AddStep(@"set rooms", () => lounge.Rooms = rooms);
selectAssert(1);
AddStep(@"open room 1", () => clickRoom(1));
AddUntilStep(() => lounge.ChildScreen?.IsCurrentScreen == true, "wait until room current");
AddStep(@"make lounge current", lounge.MakeCurrent);
filterAssert(@"THE FINAL", LoungeTab.Public, 1);
filterAssert(string.Empty, LoungeTab.Public, 2);
@ -198,6 +201,8 @@ namespace osu.Game.Tests.Visual
private class TestLounge : Lounge
{
protected override BackgroundScreen CreateBackground() => new BackgroundScreenDefault();
public IEnumerable<DrawableRoom> ChildRooms => RoomsContainer.Children.Where(r => r.MatchingFilter);
public Room SelectedRoom => Inspector.Room;

View File

@ -0,0 +1,142 @@
// 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.Allocation;
using osu.Game.Beatmaps;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets;
using osu.Game.Screens.Multi.Screens.Match;
using osu.Game.Users;
namespace osu.Game.Tests.Visual
{
[TestFixture]
public class TestCaseMatch : OsuTestCase
{
[BackgroundDependencyLoader]
private void load(RulesetStore rulesets)
{
Room room = new Room
{
Name = { Value = @"One Awesome Room" },
Status = { Value = new RoomStatusOpen() },
Availability = { Value = RoomAvailability.Public },
Type = { Value = new GameTypeTeamVersus() },
Beatmap =
{
Value = new BeatmapInfo
{
StarDifficulty = 5.02,
Ruleset = rulesets.GetRuleset(1),
Metadata = new BeatmapMetadata
{
Title = @"Paradigm Shift",
Artist = @"Morimori Atsushi",
AuthorString = @"eiri-",
},
BeatmapSet = new BeatmapSetInfo
{
OnlineInfo = new BeatmapSetOnlineInfo
{
Covers = new BeatmapSetOnlineCovers
{
Cover = @"https://assets.ppy.sh/beatmaps/765055/covers/cover.jpg?1526955337",
},
},
},
},
},
MaxParticipants = { Value = 5 },
Participants =
{
Value = new[]
{
new User
{
Username = @"eiri-",
Id = 3388410,
Country = new Country { FlagName = @"US" },
CoverUrl = @"https://assets.ppy.sh/user-profile-covers/3388410/00a8486a247831e1cc4375db519f611ac970bda8bc0057d78b0f540ea38c3e58.jpeg",
IsSupporter = true,
},
new User
{
Username = @"Nepuri",
Id = 6637817,
Country = new Country { FlagName = @"DE" },
CoverUrl = @"https://assets.ppy.sh/user-profile-covers/6637817/9085fc60248b6b5327a72c1dcdecf2dbedba810ae0ab6bcf7224e46b1339632a.jpeg",
IsSupporter = true,
},
new User
{
Username = @"goheegy",
Id = 8057655,
Country = new Country { FlagName = @"GB" },
CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8057655/21cec27c25a11dc197a4ec6a74253dbabb495949b0e0697113352f12007018c5.jpeg",
},
new User
{
Username = @"Alumetri",
Id = 5371497,
Country = new Country { FlagName = @"RU" },
CoverUrl = @"https://assets.ppy.sh/user-profile-covers/5371497/e023b8c7fbe3613e64bd4856703517ea50fbed8a5805dc9acda9efe9897c67e2.jpeg",
},
}
},
};
Match match = new Match(room);
AddStep(@"show", () => Add(match));
AddStep(@"null beatmap", () => room.Beatmap.Value = null);
AddStep(@"change name", () => room.Name.Value = @"Two Awesome Rooms");
AddStep(@"change status", () => room.Status.Value = new RoomStatusPlaying());
AddStep(@"change availability", () => room.Availability.Value = RoomAvailability.FriendsOnly);
AddStep(@"change type", () => room.Type.Value = new GameTypeTag());
AddStep(@"change beatmap", () => room.Beatmap.Value = new BeatmapInfo
{
StarDifficulty = 4.33,
Ruleset = rulesets.GetRuleset(2),
Metadata = new BeatmapMetadata
{
Title = @"Yasashisa no Riyuu",
Artist = @"ChouCho",
AuthorString = @"celerih",
},
BeatmapSet = new BeatmapSetInfo
{
OnlineInfo = new BeatmapSetOnlineInfo
{
Covers = new BeatmapSetOnlineCovers
{
Cover = @"https://assets.ppy.sh/beatmaps/685391/covers/cover.jpg?1524597970",
},
},
},
});
AddStep(@"null max participants", () => room.MaxParticipants.Value = null);
AddStep(@"change participants", () => room.Participants.Value = new[]
{
new User
{
Username = @"Spectator",
Id = 702598,
Country = new Country { FlagName = @"KR" },
CoverUrl = @"https://assets.ppy.sh/user-profile-covers/702598/3bbf4cb8b8d2cf8b03145000a975ff27e191ab99b0920832e7dd67386280e288.jpeg",
IsSupporter = true,
},
new User
{
Username = @"celerih",
Id = 4696296,
Country = new Country { FlagName = @"CA" },
CoverUrl = @"https://assets.ppy.sh/user-profile-covers/4696296/7f8500731d0ac66d5472569d146a7be07d9460273361913f22c038867baddaef.jpeg",
},
});
AddStep(@"exit", match.Exit);
}
}
}

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 NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Screens.Multi.Screens.Match;
namespace osu.Game.Tests.Visual
{
[TestFixture]
public class TestCaseMatchHeader : OsuTestCase
{
public TestCaseMatchHeader()
{
Header header = new Header();
Add(header);
AddStep(@"set beatmap set", () => header.BeatmapSet = new BeatmapSetInfo
{
OnlineInfo = new BeatmapSetOnlineInfo
{
Covers = new BeatmapSetOnlineCovers
{
Cover = @"https://assets.ppy.sh/beatmaps/760757/covers/cover.jpg?1526944540",
},
},
});
AddStep(@"change beatmap set", () => header.BeatmapSet = new BeatmapSetInfo
{
OnlineInfo = new BeatmapSetOnlineInfo
{
Covers = new BeatmapSetOnlineCovers
{
Cover = @"https://assets.ppy.sh/beatmaps/761883/covers/cover.jpg?1525557400",
},
},
});
AddStep(@"null beatmap set", () => header.BeatmapSet = null);
}
}
}

View File

@ -0,0 +1,57 @@
// 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.Allocation;
using osu.Game.Beatmaps;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets;
using osu.Game.Screens.Multi.Screens.Match;
namespace osu.Game.Tests.Visual
{
[TestFixture]
public class TestCaseMatchInfo : OsuTestCase
{
[BackgroundDependencyLoader]
private void load(RulesetStore rulesets)
{
Info info = new Info();
Add(info);
AddStep(@"set name", () => info.Name = @"Room Name?");
AddStep(@"set availability", () => info.Availability = RoomAvailability.FriendsOnly);
AddStep(@"set status", () => info.Status = new RoomStatusPlaying());
AddStep(@"set beatmap", () => info.Beatmap = new BeatmapInfo
{
StarDifficulty = 2.4,
Ruleset = rulesets.GetRuleset(0),
Metadata = new BeatmapMetadata
{
Title = @"My Song",
Artist = @"VisualTests",
AuthorString = @"osu!lazer",
},
});
AddStep(@"set type", () => info.Type = new GameTypeTagTeam());
AddStep(@"change name", () => info.Name = @"Room Name!");
AddStep(@"change availability", () => info.Availability = RoomAvailability.InviteOnly);
AddStep(@"change status", () => info.Status = new RoomStatusOpen());
AddStep(@"null beatmap", () => info.Beatmap = null);
AddStep(@"change type", () => info.Type = new GameTypeTeamVersus());
AddStep(@"change beatmap", () => info.Beatmap = new BeatmapInfo
{
StarDifficulty = 4.2,
Ruleset = rulesets.GetRuleset(3),
Metadata = new BeatmapMetadata
{
Title = @"Your Song",
Artist = @"Tester",
AuthorString = @"Someone",
},
});
}
}
}

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 NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Screens.Multi.Screens.Match;
using osu.Game.Users;
namespace osu.Game.Tests.Visual
{
[TestFixture]
public class TestCaseMatchParticipants : OsuTestCase
{
public TestCaseMatchParticipants()
{
Participants participants;
Add(participants = new Participants
{
RelativeSizeAxes = Axes.Both,
});
AddStep(@"set max to null", () => participants.Max = null);
AddStep(@"set users", () => participants.Users = new[]
{
new User
{
Username = @"Feppla",
Id = 4271601,
Country = new Country { FlagName = @"SE" },
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg",
IsSupporter = true,
},
new User
{
Username = @"Xilver",
Id = 3099689,
Country = new Country { FlagName = @"IL" },
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg",
IsSupporter = true,
},
new User
{
Username = @"Wucki",
Id = 5287410,
Country = new Country { FlagName = @"FI" },
CoverUrl = @"https://assets.ppy.sh/user-profile-covers/5287410/5cfeaa9dd41cbce038ecdc9d781396ed4b0108089170bf7f50492ef8eadeb368.jpeg",
IsSupporter = true,
},
});
AddStep(@"set max", () => participants.Max = 10);
AddStep(@"clear users", () => participants.Users = new User[] { });
AddStep(@"set max to null", () => participants.Max = null);
}
}
}

View File

@ -0,0 +1,26 @@
// 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.Graphics.Containers;
using osu.Game.Screens.Backgrounds;
namespace osu.Game.Tests.Visual
{
public class TestCaseParallaxContainer : OsuTestCase
{
public TestCaseParallaxContainer()
{
ParallaxContainer parallax;
Add(parallax = new ParallaxContainer
{
Child = new BackgroundScreenDefault { Alpha = 0.8f }
});
AddStep("default parallax", () => parallax.ParallaxAmount = ParallaxContainer.DEFAULT_PARALLAX_AMOUNT);
AddStep("high parallax", () => parallax.ParallaxAmount = ParallaxContainer.DEFAULT_PARALLAX_AMOUNT * 10);
AddStep("no parallax", () => parallax.ParallaxAmount = 0);
AddStep("negative parallax", () => parallax.ParallaxAmount = -ParallaxContainer.DEFAULT_PARALLAX_AMOUNT);
}
}
}

View File

@ -0,0 +1,127 @@
// 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.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Containers;
using osu.Game.Audio;
using osu.Game.Beatmaps;
namespace osu.Game.Tests.Visual
{
public class TestCasePreviewTrackManager : OsuTestCase, IPreviewTrackOwner
{
private readonly PreviewTrackManager trackManager = new TestPreviewTrackManager();
protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = new DependencyContainer(base.CreateLocalDependencies(parent));
dependencies.CacheAs(trackManager);
dependencies.CacheAs<IPreviewTrackOwner>(this);
return dependencies;
}
[BackgroundDependencyLoader]
private void load()
{
AddInternal(trackManager);
}
[Test]
public void TestStartStop()
{
PreviewTrack track = null;
AddStep("get track", () => track = getOwnedTrack());
AddStep("start", () => track.Start());
AddAssert("started", () => track.IsRunning);
AddStep("stop", () => track.Stop());
AddAssert("stopped", () => !track.IsRunning);
}
[Test]
public void TestStartMultipleTracks()
{
PreviewTrack track1 = null;
PreviewTrack track2 = null;
AddStep("get tracks", () =>
{
track1 = getOwnedTrack();
track2 = getOwnedTrack();
});
AddStep("start track 1", () => track1.Start());
AddStep("start track 2", () => track2.Start());
AddAssert("track 1 stopped", () => !track1.IsRunning);
AddAssert("track 2 started", () => track2.IsRunning);
}
[Test]
public void TestCancelFromOwner()
{
PreviewTrack track = null;
AddStep("get track", () => track = getOwnedTrack());
AddStep("start", () => track.Start());
AddStep("stop by owner", () => trackManager.StopAnyPlaying(this));
AddAssert("stopped", () => !track.IsRunning);
}
[Test]
public void TestCancelFromNonOwner()
{
TestTrackOwner owner = null;
PreviewTrack track = null;
AddStep("get track", () => AddInternal(owner = new TestTrackOwner(track = getTrack())));
AddStep("start", () => track.Start());
AddStep("attempt stop", () => trackManager.StopAnyPlaying(this));
AddAssert("not stopped", () => track.IsRunning);
AddStep("stop by true owner", () => trackManager.StopAnyPlaying(owner));
AddAssert("stopped", () => !track.IsRunning);
}
private PreviewTrack getTrack() => trackManager.Get(null);
private PreviewTrack getOwnedTrack()
{
var track = getTrack();
AddInternal(track);
return track;
}
private class TestTrackOwner : CompositeDrawable, IPreviewTrackOwner
{
public TestTrackOwner(PreviewTrack track)
{
AddInternal(track);
}
protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = new DependencyContainer(base.CreateLocalDependencies(parent));
dependencies.CacheAs<IPreviewTrackOwner>(this);
return dependencies;
}
}
private class TestPreviewTrackManager : PreviewTrackManager
{
protected override TrackManagerPreviewTrack CreatePreviewTrack(BeatmapSetInfo beatmapSetInfo, TrackManager trackManager) => new TestPreviewTrack(beatmapSetInfo, trackManager);
protected class TestPreviewTrack : TrackManagerPreviewTrack
{
public TestPreviewTrack(BeatmapSetInfo beatmapSetInfo, TrackManager trackManager)
: base(beatmapSetInfo, trackManager)
{
}
protected override Track GetTrack() => new TrackVirtual { Length = 100000 };
}
}
}
}

View File

@ -114,7 +114,7 @@ namespace osu.Game.Tests.Visual
private class TestPlayfield : ScrollingPlayfield
{
public readonly ScrollingDirection Direction;
public new readonly ScrollingDirection Direction;
public TestPlayfield(ScrollingDirection direction)
: base(direction)

View File

@ -0,0 +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
namespace osu.Game.Audio
{
/// <summary>
/// Interface for objects that can own <see cref="IPreviewTrack"/>s.
/// </summary>
/// <remarks>
/// <see cref="IPreviewTrackOwner"/>s can cancel the currently playing <see cref="PreviewTrack"/> through the
/// global <see cref="PreviewTrackManager"/> if they're the owner of the playing <see cref="PreviewTrack"/>.
/// </remarks>
public interface IPreviewTrackOwner
{
}
}

View File

@ -0,0 +1,103 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Graphics;
using osu.Framework.Threading;
namespace osu.Game.Audio
{
public abstract class PreviewTrack : Component
{
/// <summary>
/// Invoked when this <see cref="PreviewTrack"/> has stopped playing.
/// </summary>
public event Action Stopped;
/// <summary>
/// Invoked when this <see cref="PreviewTrack"/> has started playing.
/// </summary>
public event Action Started;
private Track track;
private bool hasStarted;
[BackgroundDependencyLoader]
private void load()
{
track = GetTrack();
}
/// <summary>
/// Length of the track.
/// </summary>
public double Length => track?.Length ?? 0;
/// <summary>
/// The current track time.
/// </summary>
public double CurrentTime => track?.CurrentTime ?? 0;
/// <summary>
/// Whether the track is loaded.
/// </summary>
public bool TrackLoaded => track?.IsLoaded ?? false;
/// <summary>
/// Whether the track is playing.
/// </summary>
public bool IsRunning => track?.IsRunning ?? false;
protected override void Update()
{
base.Update();
// Todo: Track currently doesn't signal its completion, so we have to handle it manually
if (hasStarted && track.HasCompleted)
Stop();
}
private ScheduledDelegate startDelegate;
/// <summary>
/// Starts playing this <see cref="PreviewTrack"/>.
/// </summary>
public void Start() => startDelegate = Schedule(() =>
{
if (track == null)
return;
if (hasStarted)
return;
hasStarted = true;
track.Restart();
Started?.Invoke();
});
/// <summary>
/// Stops playing this <see cref="PreviewTrack"/>.
/// </summary>
public void Stop()
{
startDelegate?.Cancel();
if (track == null)
return;
if (!hasStarted)
return;
hasStarted = false;
track.Stop();
Stopped?.Invoke();
}
/// <summary>
/// Retrieves the audio track.
/// </summary>
protected abstract Track GetTrack();
}
}

View File

@ -0,0 +1,107 @@
// 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.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Framework.IO.Stores;
using osu.Game.Beatmaps;
namespace osu.Game.Audio
{
/// <summary>
/// A central store for the retrieval of <see cref="PreviewTrack"/>s.
/// </summary>
public class PreviewTrackManager : Component
{
private readonly BindableDouble muteBindable = new BindableDouble();
private AudioManager audio;
private TrackManager trackManager;
private TrackManagerPreviewTrack current;
[BackgroundDependencyLoader]
private void load(AudioManager audio, FrameworkConfigManager config)
{
trackManager = new TrackManager(new OnlineStore());
this.audio = audio;
audio.AddItem(trackManager);
config.BindWith(FrameworkSetting.VolumeMusic, trackManager.Volume);
}
/// <summary>
/// Retrieves a <see cref="PreviewTrack"/> for a <see cref="BeatmapSetInfo"/>.
/// </summary>
/// <param name="beatmapSetInfo">The <see cref="BeatmapSetInfo"/> to retrieve the preview track for.</param>
/// <returns>The playable <see cref="PreviewTrack"/>.</returns>
public PreviewTrack Get(BeatmapSetInfo beatmapSetInfo)
{
var track = CreatePreviewTrack(beatmapSetInfo, trackManager);
track.Started += () =>
{
current?.Stop();
current = track;
audio.Track.AddAdjustment(AdjustableProperty.Volume, muteBindable);
};
track.Stopped += () =>
{
current = null;
audio.Track.RemoveAdjustment(AdjustableProperty.Volume, muteBindable);
};
return track;
}
/// <summary>
/// Stops any currently playing <see cref="PreviewTrack"/>.
/// </summary>
/// <remarks>
/// Only the immediate owner (an object that implements <see cref="IPreviewTrackOwner"/>) of the playing <see cref="PreviewTrack"/>
/// can globally stop the currently playing <see cref="PreviewTrack"/>. The object holding a reference to the <see cref="PreviewTrack"/>
/// can always stop the <see cref="PreviewTrack"/> themselves through <see cref="PreviewTrack.Stop()"/>.
/// </remarks>
/// <param name="source">The <see cref="IPreviewTrackOwner"/> which may be the owner of the <see cref="PreviewTrack"/>.</param>
public void StopAnyPlaying(IPreviewTrackOwner source)
{
if (current == null || current.Owner != source)
return;
current.Stop();
current = null;
}
/// <summary>
/// Creates the <see cref="TrackManagerPreviewTrack"/>.
/// </summary>
protected virtual TrackManagerPreviewTrack CreatePreviewTrack(BeatmapSetInfo beatmapSetInfo, TrackManager trackManager) => new TrackManagerPreviewTrack(beatmapSetInfo, trackManager);
protected class TrackManagerPreviewTrack : PreviewTrack
{
public IPreviewTrackOwner Owner { get; private set; }
private readonly BeatmapSetInfo beatmapSetInfo;
private readonly TrackManager trackManager;
public TrackManagerPreviewTrack(BeatmapSetInfo beatmapSetInfo, TrackManager trackManager)
{
this.beatmapSetInfo = beatmapSetInfo;
this.trackManager = trackManager;
}
[BackgroundDependencyLoader]
private void load(IPreviewTrackOwner owner)
{
Owner = owner;
}
protected override Track GetTrack() => trackManager.Get($"https://b.ppy.sh/preview/{beatmapSetInfo?.OnlineBeatmapSetID}.mp3");
}
}
}

View File

@ -389,6 +389,9 @@ namespace osu.Game.Beatmaps
if (!force && beatmap.OnlineBeatmapID != null && beatmap.BeatmapSet.OnlineBeatmapSetID != null)
return true;
if (api.State != APIState.Online)
return false;
Logger.Log("Attempting online lookup for IDs...", LoggingTarget.Database);
try

View File

@ -116,9 +116,6 @@ namespace osu.Game.Beatmaps
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);
@ -127,6 +124,9 @@ namespace osu.Game.Beatmaps
foreach (var obj in converted.HitObjects)
mod.ApplyToHitObject(obj);
// Post-process
rulesetInstance.CreateBeatmapProcessor(converted)?.PostProcess();
return converted;
}

View File

@ -8,20 +8,32 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using OpenTK;
using osu.Framework.Configuration;
using osu.Game.Audio;
using osu.Game.Overlays;
namespace osu.Game.Graphics.Containers
{
public class OsuFocusedOverlayContainer : FocusedOverlayContainer
public class OsuFocusedOverlayContainer : FocusedOverlayContainer, IPreviewTrackOwner
{
private SampleChannel samplePopIn;
private SampleChannel samplePopOut;
private PreviewTrackManager previewTrackManager;
protected readonly Bindable<OverlayActivation> OverlayActivationMode = new Bindable<OverlayActivation>(OverlayActivation.All);
[BackgroundDependencyLoader(true)]
private void load(OsuGame osuGame, AudioManager audio)
protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = new DependencyContainer(base.CreateLocalDependencies(parent));
dependencies.CacheAs<IPreviewTrackOwner>(this);
return dependencies;
}
[BackgroundDependencyLoader(true)]
private void load(OsuGame osuGame, AudioManager audio, PreviewTrackManager previewTrackManager)
{
this.previewTrackManager = previewTrackManager;
if (osuGame != null)
OverlayActivationMode.BindTo(osuGame.OverlayActivationMode);
@ -66,5 +78,11 @@ namespace osu.Game.Graphics.Containers
break;
}
}
protected override void PopOut()
{
base.PopOut();
previewTrackManager.StopAnyPlaying(this);
}
}
}

View File

@ -16,6 +16,9 @@ namespace osu.Game.Graphics.Containers
{
public const float DEFAULT_PARALLAX_AMOUNT = 0.02f;
/// <summary>
/// The amount of parallax movement. Negative values will reverse the direction of parallax relative to user input.
/// </summary>
public float ParallaxAmount = DEFAULT_PARALLAX_AMOUNT;
private Bindable<bool> parallaxEnabled;
@ -45,7 +48,7 @@ namespace osu.Game.Graphics.Containers
if (!parallaxEnabled)
{
content.MoveTo(Vector2.Zero, firstUpdate ? 0 : 1000, Easing.OutQuint);
content.Scale = new Vector2(1 + ParallaxAmount);
content.Scale = new Vector2(1 + System.Math.Abs(ParallaxAmount));
}
};
}
@ -69,7 +72,7 @@ namespace osu.Game.Graphics.Containers
double elapsed = MathHelper.Clamp(Clock.ElapsedFrameTime, 0, 1000);
content.Position = Interpolation.ValueAt(elapsed, content.Position, offset, 0, 1000, Easing.OutQuint);
content.Scale = Interpolation.ValueAt(elapsed, content.Scale, new Vector2(1 + ParallaxAmount), 0, 1000, Easing.OutQuint);
content.Scale = Interpolation.ValueAt(elapsed, content.Scale, new Vector2(1 + System.Math.Abs(ParallaxAmount)), 0, 1000, Easing.OutQuint);
}
firstUpdate = false;

View File

@ -4,12 +4,17 @@
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using OpenTK;
using OpenTK.Graphics;
namespace osu.Game.Graphics.UserInterface
{
public class LoadingAnimation : VisibilityContainer
{
private readonly SpriteIcon spinner;
private readonly SpriteIcon spinnerShadow;
private const float spin_duration = 600;
private const float transition_duration = 200;
public LoadingAnimation()
{
@ -20,12 +25,22 @@ namespace osu.Game.Graphics.UserInterface
Children = new Drawable[]
{
spinner = new SpriteIcon
spinnerShadow = new SpriteIcon
{
Size = new Vector2(20),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Icon = FontAwesome.fa_spinner
RelativeSizeAxes = Axes.Both,
Position = new Vector2(1, 1),
Colour = Color4.Black,
Alpha = 0.4f,
Icon = FontAwesome.fa_circle_o_notch
},
spinner = new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Icon = FontAwesome.fa_circle_o_notch
}
};
}
@ -34,12 +49,11 @@ namespace osu.Game.Graphics.UserInterface
{
base.LoadComplete();
spinner.Spin(2000, RotationDirection.Clockwise);
spinner.Spin(spin_duration, RotationDirection.Clockwise);
spinnerShadow.Spin(spin_duration, RotationDirection.Clockwise);
}
private const float transition_duration = 500;
protected override void PopIn() => this.FadeIn(transition_duration * 5, Easing.OutQuint);
protected override void PopIn() => this.FadeIn(transition_duration * 2, Easing.OutQuint);
protected override void PopOut() => this.FadeOut(transition_duration, Easing.OutQuint);
}

View File

@ -32,16 +32,14 @@ namespace osu.Game.Input.Handlers
public override int Priority => 0;
public class ReplayState<T> : InputState
public class ReplayState<T> : IInput
where T : struct
{
public List<T> PressedActions;
public override InputState Clone()
public void Apply(InputState state, IInputStateChangeHandler handler)
{
var clone = (ReplayState<T>)base.Clone();
clone.PressedActions = new List<T>(PressedActions);
return clone;
handler.HandleCustomInput(state, this);
}
}
}

View File

@ -0,0 +1,376 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using osu.Game.Database;
namespace osu.Game.Migrations
{
[DbContext(typeof(OsuDbContext))]
[Migration("20180621044111_UpdateTaikoDefaultBindings")]
partial class UpdateTaikoDefaultBindings
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.1.1-rtm-30846");
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<float>("ApproachRate");
b.Property<float>("CircleSize");
b.Property<float>("DrainRate");
b.Property<float>("OverallDifficulty");
b.Property<double>("SliderMultiplier");
b.Property<double>("SliderTickRate");
b.HasKey("ID");
b.ToTable("BeatmapDifficulty");
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<int>("AudioLeadIn");
b.Property<int>("BaseDifficultyID");
b.Property<int>("BeatDivisor");
b.Property<int>("BeatmapSetInfoID");
b.Property<bool>("Countdown");
b.Property<double>("DistanceSpacing");
b.Property<int>("GridSize");
b.Property<string>("Hash");
b.Property<bool>("Hidden");
b.Property<bool>("LetterboxInBreaks");
b.Property<string>("MD5Hash");
b.Property<int?>("MetadataID");
b.Property<int?>("OnlineBeatmapID");
b.Property<string>("Path");
b.Property<int>("RulesetID");
b.Property<bool>("SpecialStyle");
b.Property<float>("StackLeniency");
b.Property<double>("StarDifficulty");
b.Property<string>("StoredBookmarks");
b.Property<double>("TimelineZoom");
b.Property<string>("Version");
b.Property<bool>("WidescreenStoryboard");
b.HasKey("ID");
b.HasIndex("BaseDifficultyID");
b.HasIndex("BeatmapSetInfoID");
b.HasIndex("Hash");
b.HasIndex("MD5Hash");
b.HasIndex("MetadataID");
b.HasIndex("OnlineBeatmapID")
.IsUnique();
b.HasIndex("RulesetID");
b.ToTable("BeatmapInfo");
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<string>("Artist");
b.Property<string>("ArtistUnicode");
b.Property<string>("AudioFile");
b.Property<string>("AuthorString")
.HasColumnName("Author");
b.Property<string>("BackgroundFile");
b.Property<int>("PreviewTime");
b.Property<string>("Source");
b.Property<string>("Tags");
b.Property<string>("Title");
b.Property<string>("TitleUnicode");
b.HasKey("ID");
b.ToTable("BeatmapMetadata");
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<int>("BeatmapSetInfoID");
b.Property<int>("FileInfoID");
b.Property<string>("Filename")
.IsRequired();
b.HasKey("ID");
b.HasIndex("BeatmapSetInfoID");
b.HasIndex("FileInfoID");
b.ToTable("BeatmapSetFileInfo");
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<bool>("DeletePending");
b.Property<string>("Hash");
b.Property<int?>("MetadataID");
b.Property<int?>("OnlineBeatmapSetID");
b.Property<bool>("Protected");
b.HasKey("ID");
b.HasIndex("DeletePending");
b.HasIndex("Hash")
.IsUnique();
b.HasIndex("MetadataID");
b.HasIndex("OnlineBeatmapSetID")
.IsUnique();
b.ToTable("BeatmapSetInfo");
});
modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<int>("IntKey")
.HasColumnName("Key");
b.Property<int?>("RulesetID");
b.Property<string>("StringValue")
.HasColumnName("Value");
b.Property<int?>("Variant");
b.HasKey("ID");
b.HasIndex("RulesetID", "Variant");
b.ToTable("Settings");
});
modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<int>("IntAction")
.HasColumnName("Action");
b.Property<string>("KeysString")
.HasColumnName("Keys");
b.Property<int?>("RulesetID");
b.Property<int?>("Variant");
b.HasKey("ID");
b.HasIndex("IntAction");
b.HasIndex("RulesetID", "Variant");
b.ToTable("KeyBinding");
});
modelBuilder.Entity("osu.Game.IO.FileInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<string>("Hash");
b.Property<int>("ReferenceCount");
b.HasKey("ID");
b.HasIndex("Hash")
.IsUnique();
b.HasIndex("ReferenceCount");
b.ToTable("FileInfo");
});
modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b =>
{
b.Property<int?>("ID")
.ValueGeneratedOnAdd();
b.Property<bool>("Available");
b.Property<string>("InstantiationInfo");
b.Property<string>("Name");
b.Property<string>("ShortName");
b.HasKey("ID");
b.HasIndex("Available");
b.HasIndex("ShortName")
.IsUnique();
b.ToTable("RulesetInfo");
});
modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<int>("FileInfoID");
b.Property<string>("Filename")
.IsRequired();
b.Property<int>("SkinInfoID");
b.HasKey("ID");
b.HasIndex("FileInfoID");
b.HasIndex("SkinInfoID");
b.ToTable("SkinFileInfo");
});
modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<string>("Creator");
b.Property<bool>("DeletePending");
b.Property<string>("Name");
b.HasKey("ID");
b.ToTable("SkinInfo");
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
{
b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty")
.WithMany()
.HasForeignKey("BaseDifficultyID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet")
.WithMany("Beatmaps")
.HasForeignKey("BeatmapSetInfoID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
.WithMany("Beatmaps")
.HasForeignKey("MetadataID");
b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
.WithMany()
.HasForeignKey("RulesetID")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
{
b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo")
.WithMany("Files")
.HasForeignKey("BeatmapSetInfoID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
.WithMany()
.HasForeignKey("FileInfoID")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
{
b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
.WithMany("BeatmapSets")
.HasForeignKey("MetadataID");
});
modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
{
b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
.WithMany()
.HasForeignKey("FileInfoID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.Skinning.SkinInfo")
.WithMany("Files")
.HasForeignKey("SkinInfoID")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,19 @@
using Microsoft.EntityFrameworkCore.Migrations;
using osu.Framework.Logging;
namespace osu.Game.Migrations
{
public partial class UpdateTaikoDefaultBindings : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("DELETE FROM KeyBinding WHERE RulesetID = 1");
Logger.Log("osu!taiko bindings have been reset due to new defaults", LoggingTarget.Runtime, LogLevel.Important);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// we can't really tell if these should be restored or not, so let's just not do so.
}
}
}

View File

@ -1,11 +1,9 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using osu.Game.Database;
using System;
namespace osu.Game.Migrations
{
@ -16,7 +14,7 @@ namespace osu.Game.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.0.3-rtm-10026");
.HasAnnotation("ProductVersion", "2.1.1-rtm-30846");
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b =>
{

View File

@ -148,7 +148,7 @@ namespace osu.Game.Online.API
// The Success callback event is fired on the main thread, so we should wait for that to run before proceeding.
// Without this, we will end up circulating this Connecting loop multiple times and queueing up many web requests
// before actually going online.
while (State != APIState.Online)
while (State > APIState.Offline && State < APIState.Online)
Thread.Sleep(500);
break;
@ -158,7 +158,6 @@ namespace osu.Game.Online.API
if (authentication.RequestAccessToken() == null)
{
Logout(false);
State = APIState.Offline;
continue;
}
@ -208,6 +207,14 @@ namespace osu.Game.Online.API
{
HttpStatusCode statusCode = (we.Response as HttpWebResponse)?.StatusCode ?? (we.Status == WebExceptionStatus.UnknownError ? HttpStatusCode.NotAcceptable : HttpStatusCode.RequestTimeout);
// special cases for un-typed but useful message responses.
switch (we.Message)
{
case "Unauthorized":
statusCode = HttpStatusCode.Unauthorized;
break;
}
switch (statusCode)
{
case HttpStatusCode.Unauthorized:
@ -292,6 +299,7 @@ namespace osu.Game.Online.API
password = null;
authentication.Clear();
LocalUser.Value = createGuestUser();
State = APIState.Offline;
}
private static User createGuestUser() => new User

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