Merge branch 'master' into fix-dialog-overlay-sounds

This commit is contained in:
Dean Herbert 2018-06-27 12:21:29 +09:00 committed by GitHub
commit bebf47d0d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
106 changed files with 3911 additions and 2076 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() public StableStorage()
: base(string.Empty) : base(string.Empty, null)
{ {
} }
} }

View File

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

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Newtonsoft.Json;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.MathUtils; using osu.Framework.MathUtils;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
@ -17,6 +18,8 @@ namespace osu.Game.Rulesets.Catch.Tests
protected override string ResourceAssembly => "osu.Game.Rulesets.Catch"; protected override string ResourceAssembly => "osu.Game.Rulesets.Catch";
[TestCase("basic"), Ignore("See: https://github.com/ppy/osu/issues/2232")] [TestCase("basic"), Ignore("See: https://github.com/ppy/osu/issues/2232")]
[TestCase("spinner")]
[TestCase("spinner-and-circles")]
public new void Test(string name) public new void Test(string name)
{ {
base.Test(name); base.Test(name);
@ -27,22 +30,15 @@ namespace osu.Game.Rulesets.Catch.Tests
if (hitObject is JuiceStream stream) if (hitObject is JuiceStream stream)
{ {
foreach (var nested in stream.NestedHitObjects) foreach (var nested in stream.NestedHitObjects)
{ yield return new ConvertValue((CatchHitObject)nested);
yield return new ConvertValue }
{ else if (hitObject is BananaShower shower)
StartTime = nested.StartTime, {
Position = ((CatchHitObject)nested).X * CatchPlayfield.BASE_WIDTH foreach (var nested in shower.NestedHitObjects)
}; yield return new ConvertValue((CatchHitObject)nested);
}
} }
else else
{ yield return new ConvertValue((CatchHitObject)hitObject);
yield return new ConvertValue
{
StartTime = hitObject.StartTime,
Position = ((CatchHitObject)hitObject).X * CatchPlayfield.BASE_WIDTH
};
}
} }
protected override Ruleset CreateRuleset() => new CatchRuleset(); protected override Ruleset CreateRuleset() => new CatchRuleset();
@ -55,8 +51,31 @@ namespace osu.Game.Rulesets.Catch.Tests
/// </summary> /// </summary>
private const float conversion_lenience = 2; private const float conversion_lenience = 2;
public double StartTime; [JsonIgnore]
public float Position; 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) public bool Equals(ConvertValue other)
=> Precision.AlmostEquals(StartTime, other.StartTime, conversion_lenience) => 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

@ -27,22 +27,6 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
var comboData = obj as IHasCombo; var comboData = obj as IHasCombo;
var endTime = obj as IHasEndTime; 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;
}
if (curveData != null) if (curveData != null)
{ {
yield return new JuiceStream yield return new JuiceStream
@ -54,20 +38,30 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
Distance = curveData.Distance, Distance = curveData.Distance,
RepeatSamples = curveData.RepeatSamples, RepeatSamples = curveData.RepeatSamples,
RepeatCount = curveData.RepeatCount, RepeatCount = curveData.RepeatCount,
X = positionData.X / CatchPlayfield.BASE_WIDTH, X = (positionData?.X ?? 0) / CatchPlayfield.BASE_WIDTH,
NewCombo = comboData?.NewCombo ?? false NewCombo = comboData?.NewCombo ?? false
}; };
yield break;
} }
else if (endTime != null)
yield return new Fruit
{ {
StartTime = obj.StartTime, yield return new BananaShower
Samples = obj.Samples, {
NewCombo = comboData?.NewCombo ?? false, StartTime = obj.StartTime,
X = positionData.X / CatchPlayfield.BASE_WIDTH Samples = obj.Samples,
}; Duration = endTime.Duration,
NewCombo = comboData?.NewCombo ?? false
};
}
else
{
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(); 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.Catch.UI;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using OpenTK; using OpenTK;
using osu.Game.Rulesets.Catch.MathUtils;
namespace osu.Game.Rulesets.Catch.Beatmaps namespace osu.Game.Rulesets.Catch.Beatmaps
{ {
@ -21,13 +22,54 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
public override void PostProcess() public override void PostProcess()
{ {
applyPositionOffsets();
initialiseHyperDash((List<CatchHitObject>)Beatmap.HitObjects); initialiseHyperDash((List<CatchHitObject>)Beatmap.HitObjects);
base.PostProcess(); base.PostProcess();
int index = 0; int index = 0;
foreach (var obj in Beatmap.HitObjects.OfType<CatchHitObject>()) foreach (var obj in Beatmap.HitObjects.OfType<CatchHitObject>())
{
obj.IndexInBeatmap = index++; 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) 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>. // Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
namespace osu.Game.Rulesets.Catch.Difficulty namespace osu.Game.Rulesets.Catch.Difficulty
{ {
public class CatchDifficultyCalculator : DifficultyCalculator 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) public CatchDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, 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>. // Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.MathUtils;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Catch.Objects namespace osu.Game.Rulesets.Catch.Objects
@ -31,8 +30,7 @@ namespace osu.Game.Rulesets.Catch.Objects
AddNested(new Banana AddNested(new Banana
{ {
Samples = Samples, Samples = Samples,
StartTime = i, StartTime = i
X = RNG.NextSingle()
}); });
} }

View File

@ -24,6 +24,11 @@ namespace osu.Game.Rulesets.Catch.Objects
public int ComboIndex { get; set; } 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> /// <summary>
/// The next fruit starts a new combo. Used for explodey. /// The next fruit starts a new combo. Used for explodey.
/// </summary> /// </summary>

View File

@ -42,7 +42,6 @@ namespace osu.Game.Rulesets.Catch.Objects
protected override void CreateNestedHitObjects() protected override void CreateNestedHitObjects()
{ {
base.CreateNestedHitObjects(); base.CreateNestedHitObjects();
createTicks(); createTicks();
} }
@ -124,9 +123,6 @@ namespace osu.Game.Rulesets.Catch.Objects
X = X + Curve.PositionAt(reversed ? 0 : 1).X / CatchPlayfield.BASE_WIDTH 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; public double EndTime => StartTime + this.SpanCount() * Curve.Distance / Velocity;

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>(); var actions = new List<CatchAction>();
@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Catch.Replays
else if (Position.Value < CurrentFrame.Position) else if (Position.Value < CurrentFrame.Position)
actions.Add(CatchAction.MoveLeft); actions.Add(CatchAction.MoveLeft);
return new List<InputState> return new List<IInput>
{ {
new CatchReplayState 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.Catch.Replays;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
using OpenTK; using OpenTK;
using OpenTK.Graphics; using OpenTK.Graphics;
@ -93,7 +94,7 @@ namespace osu.Game.Rulesets.Catch.UI
{ {
base.UpdateAfterChildren(); base.UpdateAfterChildren();
var state = GetContainingInputManager().CurrentState as CatchFramedReplayInputHandler.CatchReplayState; var state = (GetContainingInputManager().CurrentState as RulesetInputManagerInputState<CatchAction>)?.LastReplayState as CatchFramedReplayInputHandler.CatchReplayState;
if (state?.CatcherX != null) if (state?.CatcherX != null)
MovableCatcher.X = state.CatcherX.Value; MovableCatcher.X = state.CatcherX.Value;
@ -105,6 +106,11 @@ namespace osu.Game.Rulesets.Catch.UI
public class Catcher : Container, IKeyBindingHandler<CatchAction> 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; private Container<DrawableHitObject> caughtFruit;
public Container ExplodingFruitTarget; public Container ExplodingFruitTarget;
@ -232,63 +238,74 @@ namespace osu.Game.Rulesets.Catch.UI
/// <returns>Whether the catch is possible.</returns> /// <returns>Whether the catch is possible.</returns>
public bool AttemptCatch(CatchHitObject fruit) 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. // this stuff wil disappear once we move fruit to non-relative coordinate space in the future.
var catchObjectPosition = fruit.X * CatchPlayfield.BASE_WIDTH; var catchObjectPosition = fruit.X * CatchPlayfield.BASE_WIDTH;
var catcherPosition = Position.X * CatchPlayfield.BASE_WIDTH; var catcherPosition = Position.X * CatchPlayfield.BASE_WIDTH;
var validCatch = var validCatch =
catchObjectPosition >= catcherPosition - halfCatcherWidth && catchObjectPosition >= catcherPosition - halfCatchWidth &&
catchObjectPosition <= catcherPosition + halfCatcherWidth; catchObjectPosition <= catcherPosition + halfCatchWidth;
if (validCatch && fruit.HyperDash) if (validCatch && fruit.HyperDash)
{ {
HyperDashModifier = Math.Abs(fruit.HyperDashTarget.X - fruit.X) / Math.Abs(fruit.HyperDashTarget.StartTime - fruit.StartTime) / BASE_SPEED; var target = fruit.HyperDashTarget;
HyperDashDirection = fruit.HyperDashTarget.X - fruit.X; 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 else
HyperDashModifier = 1; {
SetHyperdashState();
}
return validCatch; return validCatch;
} }
private double hyperDashModifier = 1;
private int hyperDashDirection;
private float hyperDashTargetPosition;
/// <summary> /// <summary>
/// Whether we are hypderdashing or not. /// Whether we are hypderdashing or not.
/// </summary> /// </summary>
public bool HyperDashing => hyperDashModifier != 1; public bool HyperDashing => hyperDashModifier != 1;
private double hyperDashModifier = 1;
/// <summary> /// <summary>
/// The direction in which hyperdash is allowed. 0 allows both directions. /// Set hyperdash state.
/// </summary> /// </summary>
public double HyperDashDirection; /// <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>
/// <summary> public void SetHyperdashState(double modifier = 1, float targetPosition = -1)
/// The speed modifier resultant from hyperdash. Will trigger hyperdash when not equal to 1.
/// </summary>
public double HyperDashModifier
{ {
get { return hyperDashModifier; } const float hyperdash_transition_length = 180;
set
bool previouslyHyperDashing = HyperDashing;
if (modifier <= 1 || X == targetPosition)
{ {
if (value == hyperDashModifier) return; hyperDashModifier = 1;
hyperDashModifier = value; hyperDashDirection = 0;
const float transition_length = 180; if (previouslyHyperDashing)
if (HyperDashing)
{ {
this.FadeColour(Color4.OrangeRed, transition_length, Easing.OutQuint); this.FadeColour(Color4.White, hyperdash_transition_length, Easing.OutQuint);
this.FadeTo(0.2f, transition_length, Easing.OutQuint); this.FadeTo(1, hyperdash_transition_length, Easing.OutQuint);
Trail = true;
} }
else }
else
{
hyperDashModifier = modifier;
hyperDashDirection = Math.Sign(targetPosition - X);
hyperDashTargetPosition = targetPosition;
if (!previouslyHyperDashing)
{ {
HyperDashDirection = 0; this.FadeColour(Color4.OrangeRed, hyperdash_transition_length, Easing.OutQuint);
this.FadeColour(Color4.White, transition_length, Easing.OutQuint); this.FadeTo(0.2f, hyperdash_transition_length, Easing.OutQuint);
this.FadeTo(1, transition_length, Easing.OutQuint); Trail = true;
} }
} }
} }
@ -343,12 +360,18 @@ namespace osu.Game.Rulesets.Catch.UI
var direction = Math.Sign(currentDirection); var direction = Math.Sign(currentDirection);
double dashModifier = Dashing ? 1 : 0.5; double dashModifier = Dashing ? 1 : 0.5;
double speed = BASE_SPEED * dashModifier * hyperDashModifier;
if (hyperDashModifier != 1 && (HyperDashDirection == 0 || direction == Math.Sign(HyperDashDirection)))
dashModifier = hyperDashModifier;
Scale = new Vector2(Math.Abs(Scale.X) * direction, Scale.Y); 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> /// <summary>

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; 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; bool canGenerateTwoNotes = (convertType & PatternType.LowProbability) == 0;
canGenerateTwoNotes &= HitObject.Samples.Any(isDoubleSample) || sampleInfoListAt(HitObject.StartTime).Any(isDoubleSample); canGenerateTwoNotes &= HitObject.Samples.Any(isDoubleSample) || sampleInfoListAt(HitObject.StartTime).Any(isDoubleSample);

View File

@ -4,6 +4,7 @@
using osu.Framework.Configuration.Tracking; using osu.Framework.Configuration.Tracking;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Configuration;
using osu.Game.Rulesets.Mania.UI;
namespace osu.Game.Rulesets.Mania.Configuration namespace osu.Game.Rulesets.Mania.Configuration
{ {
@ -19,6 +20,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
base.InitialiseDefaults(); base.InitialiseDefaults();
Set(ManiaSetting.ScrollTime, 1500.0, 50.0, 10000.0, 50.0); Set(ManiaSetting.ScrollTime, 1500.0, 50.0, 10000.0, 50.0);
Set(ManiaSetting.ScrollDirection, ManiaScrollingDirection.Down);
} }
public override TrackedSettings CreateTrackedSettings() => new TrackedSettings public override TrackedSettings CreateTrackedSettings() => new TrackedSettings
@ -29,6 +31,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
public enum ManiaSetting 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) protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double timeRate)
{ {
if (!beatmap.HitObjects.Any())
return new ManiaDifficultyAttributes(mods, 0);
var difficultyHitObjects = new List<ManiaHitObjectDifficulty>(); var difficultyHitObjects = new List<ManiaHitObjectDifficulty>();
int columnCount = ((ManiaBeatmap)beatmap).TotalColumns; int columnCount = ((ManiaBeatmap)beatmap).TotalColumns;
@ -50,9 +53,14 @@ namespace osu.Game.Rulesets.Mania.Difficulty
if (!calculateStrainValues(difficultyHitObjects, timeRate)) if (!calculateStrainValues(difficultyHitObjects, timeRate))
return new DifficultyAttributes(mods, 0); return new DifficultyAttributes(mods, 0);
double starRating = calculateDifficulty(difficultyHitObjects, timeRate) * star_scaling_factor; 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) private bool calculateStrainValues(List<ManiaHitObjectDifficulty> objects, double timeRate)

View File

@ -13,6 +13,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty
{ {
public class ManiaPerformanceCalculator : PerformanceCalculator public class ManiaPerformanceCalculator : PerformanceCalculator
{ {
protected new ManiaDifficultyAttributes Attributes => (ManiaDifficultyAttributes)base.Attributes;
private Mod[] mods; private Mod[] mods;
// Score after being scaled by non-difficulty-increasing 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) 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 if (Attributes.GreatHitWindow <= 0)
double hitWindowGreat = (int)(Beatmap.HitObjects.First().HitWindows.Great / 2) / TimeRate;
if (hitWindowGreat <= 0)
return 0; return 0;
// Lots of arbitrary values from testing. // Lots of arbitrary values from testing.
// Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution // 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 * strainValue
* Math.Pow(Math.Max(0.0, scaledScore - 960000) / 40000, 1.1); * 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.Rulesets.Replays.Types;
using osu.Game.Beatmaps.Legacy; using osu.Game.Beatmaps.Legacy;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Configuration;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mania.Beatmaps; 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 IRulesetConfigManager CreateConfig(SettingsStore settings) => new ManiaConfigManager(settings, RulesetInfo);
public override RulesetSettingsSubsection CreateSettings() => new ManiaSettingsSubsection(this);
public ManiaRuleset(RulesetInfo rulesetInfo = null) public ManiaRuleset(RulesetInfo rulesetInfo = null)
: base(rulesetInfo) : 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.Game.Rulesets.Mania.Judgements;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI.Scrolling;
namespace osu.Game.Rulesets.Mania.Objects.Drawables namespace osu.Game.Rulesets.Mania.Objects.Drawables
{ {
@ -75,6 +76,13 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
AddNested(tail); 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 public override Color4 AccentColour
{ {
get { return base.AccentColour; } get { return base.AccentColour; }
@ -100,7 +108,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
base.Update(); base.Update();
// Make the body piece not lie under the head note // 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; 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>. // Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
namespace osu.Game.Rulesets.Mania.Objects.Drawables namespace osu.Game.Rulesets.Mania.Objects.Drawables
{ {
@ -16,18 +20,29 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
public new TObject HitObject; public new TObject HitObject;
protected readonly IBindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();
protected DrawableManiaHitObject(TObject hitObject, ManiaAction? action = null) protected DrawableManiaHitObject(TObject hitObject, ManiaAction? action = null)
: base(hitObject) : base(hitObject)
{ {
Anchor = Anchor.TopCentre;
Origin = Anchor.TopCentre;
HitObject = hitObject; HitObject = hitObject;
if (action != null) if (action != null)
Action = action.Value; 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) protected override void UpdateState(ArmedState state)
{ {
switch (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.Judgements;
using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI.Scrolling;
namespace osu.Game.Rulesets.Mania.Objects.Drawables namespace osu.Game.Rulesets.Mania.Objects.Drawables
{ {
@ -28,14 +29,14 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
CornerRadius = 5; CornerRadius = 5;
Masking = true; Masking = true;
InternalChildren = new Drawable[] InternalChild = headPiece = new NotePiece();
{ }
headPiece = new NotePiece
{ protected override void OnDirectionChanged(ScrollingDirection direction)
Anchor = Anchor.TopCentre, {
Origin = Anchor.TopCentre base.OnDirectionChanged(direction);
}
}; headPiece.Anchor = headPiece.Origin = direction == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
} }
public override Color4 AccentColour public override Color4 AccentColour

View File

@ -1,12 +1,16 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>. // Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Allocation;
using osu.Framework.Configuration;
using OpenTK.Graphics; using OpenTK.Graphics;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.UI.Scrolling;
namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces 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; public const float NOTE_HEIGHT = 10;
private const float head_colour_height = 6; private const float head_colour_height = 6;
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
private readonly Box colouredBox; private readonly Box colouredBox;
public NotePiece() public NotePiece()
@ -33,8 +39,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
}, },
colouredBox = new Box colouredBox = new Box
{ {
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Height = head_colour_height, Height = head_colour_height,
Alpha = 0.2f 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; private Color4 accentColour;
public 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(); 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>. // Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using OpenTK;
using OpenTK.Graphics; using OpenTK.Graphics;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Colour;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using System;
using System.Linq; using System.Linq;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.UI.Components;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
namespace osu.Game.Rulesets.Mania.UI 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 column_width = 45;
private const float special_column_width = 70; private const float special_column_width = 70;
public ManiaAction Action; private ManiaAction action;
private readonly Box background; public ManiaAction Action
private readonly Box backgroundOverlay; {
private readonly Container hitTargetBar; get => action;
private readonly Container keyIcon; 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; internal readonly Container TopLevelContainer;
private readonly Container explosionContainer; private readonly Container explosionContainer;
protected override Container<Drawable> Content => content; protected override Container<Drawable> Content => hitObjectArea;
private readonly Container<Drawable> content;
public Column() public Column(ScrollingDirection direction)
: base(ScrollingDirection.Up) : base(direction)
{ {
RelativeSizeAxes = Axes.Y; RelativeSizeAxes = Axes.Y;
Width = column_width; Width = column_width;
@ -52,71 +53,21 @@ namespace osu.Game.Rulesets.Mania.UI
Masking = true; Masking = true;
CornerRadius = 5; CornerRadius = 5;
InternalChildren = new Drawable[] background = new ColumnBackground { RelativeSizeAxes = Axes.Both };
Container hitTargetContainer;
InternalChildren = new[]
{ {
background = new Box // For input purposes, the background is added at the highest depth, but is then proxied back below all other elements
{ background.CreateProxy(),
Name = "Background", hitTargetContainer = new Container
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
{ {
Name = "Hit target + hit objects", Name = "Hit target + hit objects",
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Top = ManiaStage.HIT_TARGET_POSITION },
Children = new Drawable[] Children = new Drawable[]
{ {
new Container hitObjectArea = new ColumnHitObjectArea { RelativeSizeAxes = Axes.Both },
{
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
},
explosionContainer = new Container explosionContainer = new Container
{ {
Name = "Hit explosions", Name = "Hit explosions",
@ -124,46 +75,27 @@ namespace osu.Game.Rulesets.Mania.UI
} }
} }
}, },
new Container keyArea = new ColumnKeyArea
{ {
Name = "Key",
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Height = ManiaStage.HIT_TARGET_POSITION, 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 = new Container { RelativeSizeAxes = Axes.Both }
}; };
TopLevelContainer.Add(explosionContainer.CreateProxy()); 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; public override Axes RelativeSizeAxes => Axes.Y;
@ -192,22 +124,9 @@ namespace osu.Game.Rulesets.Mania.UI
return; return;
accentColour = value; accentColour = value;
background.Colour = accentColour; background.AccentColour = value;
backgroundOverlay.Colour = ColourInfo.GradientVertical(accentColour.Opacity(0.6f), accentColour.Opacity(0)); keyArea.AccentColour = value;
hitObjectArea.AccentColour = value;
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),
};
} }
} }
@ -228,48 +147,10 @@ namespace osu.Game.Rulesets.Mania.UI
if (!judgement.IsHit || !judgedObject.DisplayJudgement) if (!judgement.IsHit || !judgedObject.DisplayJudgement)
return; return;
explosionContainer.Add(new HitExplosion(judgedObject)); explosionContainer.Add(new HitExplosion(judgedObject)
}
private bool onPressed(ManiaAction action)
{
if (action == Action)
{ {
backgroundOverlay.FadeTo(1, 50, Easing.OutQuint).Then().FadeTo(0.5f, 250, Easing.OutQuint); Anchor = Direction == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre
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;
} }
public bool OnPressed(ManiaAction action) 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; bool isTick = judgedObject is DrawableHoldNoteTick;
Anchor = Anchor.TopCentre;
Origin = Anchor.Centre; Origin = Anchor.Centre;
RelativeSizeAxes = Axes.X; 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.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
@ -17,18 +16,13 @@ using osu.Game.Rulesets.UI.Scrolling;
namespace osu.Game.Rulesets.Mania.UI 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(); public List<Column> Columns => stages.SelectMany(x => x.Columns).ToList();
private readonly List<ManiaStage> stages = new List<ManiaStage>(); private readonly List<ManiaStage> stages = new List<ManiaStage>();
public ManiaPlayfield(List<StageDefinition> stageDefinitions) public ManiaPlayfield(ScrollingDirection direction, List<StageDefinition> stageDefinitions)
: base(ScrollingDirection.Up) : base(direction)
{ {
if (stageDefinitions == null) if (stageDefinitions == null)
throw new ArgumentNullException(nameof(stageDefinitions)); throw new ArgumentNullException(nameof(stageDefinitions));
@ -36,8 +30,6 @@ namespace osu.Game.Rulesets.Mania.UI
if (stageDefinitions.Count <= 0) if (stageDefinitions.Count <= 0)
throw new ArgumentException("Can't have zero or fewer stages."); throw new ArgumentException("Can't have zero or fewer stages.");
Inverted.Value = true;
GridContainer playfieldGrid; GridContainer playfieldGrid;
InternalChild = playfieldGrid = new GridContainer InternalChild = playfieldGrid = new GridContainer
{ {
@ -50,9 +42,8 @@ namespace osu.Game.Rulesets.Mania.UI
int firstColumnIndex = 0; int firstColumnIndex = 0;
for (int i = 0; i < stageDefinitions.Count; i++) 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.VisibleTimeRange.BindTo(VisibleTimeRange);
newStage.Inverted.BindTo(Inverted);
playfieldGrid.Content[0][i] = newStage; playfieldGrid.Content[0][i] = newStage;

View File

@ -4,6 +4,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Input; using osu.Framework.Input;
@ -12,6 +13,7 @@ using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Input.Handlers; using osu.Game.Input.Handlers;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Objects.Drawables;
@ -33,6 +35,9 @@ namespace osu.Game.Rulesets.Mania.UI
public IEnumerable<BarLine> BarLines; public IEnumerable<BarLine> BarLines;
private readonly Bindable<ManiaScrollingDirection> configDirection = new Bindable<ManiaScrollingDirection>();
private ScrollingInfo scrollingInfo;
public ManiaRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) public ManiaRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap) : base(ruleset, beatmap)
{ {
@ -65,12 +70,24 @@ namespace osu.Game.Rulesets.Mania.UI
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load(ManiaConfigManager config)
{ {
BarLines.ForEach(Playfield.Add); 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, Anchor = Anchor.Centre,
Origin = 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 Vector2 PlayfieldArea => new Vector2(1, 0.8f);
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new ManiaFramedReplayInputHandler(replay); 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.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
@ -24,20 +23,15 @@ namespace osu.Game.Rulesets.Mania.UI
/// <summary> /// <summary>
/// A collection of <see cref="Column"/>s. /// A collection of <see cref="Column"/>s.
/// </summary> /// </summary>
internal class ManiaStage : ScrollingPlayfield internal class ManiaStage : ManiaScrollingPlayfield
{ {
public const float HIT_TARGET_POSITION = 50; 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; public IReadOnlyList<Column> Columns => columnFlow.Children;
private readonly FillFlowContainer<Column> columnFlow; private readonly FillFlowContainer<Column> columnFlow;
protected override Container<Drawable> Content => content; protected override Container<Drawable> Content => barLineContainer;
private readonly Container<Drawable> content; private readonly Container<Drawable> barLineContainer;
public Container<DrawableManiaJudgement> Judgements => judgements; public Container<DrawableManiaJudgement> Judgements => judgements;
private readonly JudgementContainer<DrawableManiaJudgement> judgements; private readonly JudgementContainer<DrawableManiaJudgement> judgements;
@ -49,8 +43,8 @@ namespace osu.Game.Rulesets.Mania.UI
private readonly int firstColumnIndex; private readonly int firstColumnIndex;
public ManiaStage(int firstColumnIndex, StageDefinition definition, ref ManiaAction normalColumnStartAction, ref ManiaAction specialColumnStartAction) public ManiaStage(ScrollingDirection direction, int firstColumnIndex, StageDefinition definition, ref ManiaAction normalColumnStartAction, ref ManiaAction specialColumnStartAction)
: base(ScrollingDirection.Up) : base(direction)
{ {
this.firstColumnIndex = firstColumnIndex; 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 Width = 1366, // Bar lines should only be masked on the vertical axis
BypassAutoSizeAxes = Axes.Both, BypassAutoSizeAxes = Axes.Both,
Masking = true, Masking = true,
Child = content = new Container Child = barLineContainer = new Container
{ {
Name = "Bar lines", Name = "Bar lines",
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
Padding = new MarginPadding { Top = HIT_TARGET_POSITION }
} }
}, },
judgements = new JudgementContainer<DrawableManiaJudgement> judgements = new JudgementContainer<DrawableManiaJudgement>
@ -131,7 +124,7 @@ namespace osu.Game.Rulesets.Mania.UI
for (int i = 0; i < definition.Columns; i++) for (int i = 0; i < definition.Columns; i++)
{ {
var isSpecial = definition.IsSpecialColumn(i); var isSpecial = definition.IsSpecialColumn(i);
var column = new Column var column = new Column(direction)
{ {
IsSpecial = isSpecial, IsSpecial = isSpecial,
Action = isSpecial ? specialColumnStartAction++ : normalColumnStartAction++ Action = isSpecial ? specialColumnStartAction++ : normalColumnStartAction++
@ -140,14 +133,14 @@ namespace osu.Game.Rulesets.Mania.UI
AddColumn(column); AddColumn(column);
} }
Inverted.ValueChanged += invertedChanged; Direction.BindValueChanged(d =>
Inverted.TriggerChange(); {
} barLineContainer.Padding = new MarginPadding
{
private void invertedChanged(bool newValue) Top = d == ScrollingDirection.Up ? HIT_TARGET_POSITION : 0,
{ Bottom = d == ScrollingDirection.Down ? HIT_TARGET_POSITION : 0,
Scale = new Vector2(1, newValue ? -1 : 1); };
Judgements.Scale = Scale; }, true);
} }
public void AddColumn(Column c) 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 // 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 // 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

@ -10,6 +10,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{ {
public double AimStrain; public double AimStrain;
public double SpeedStrain; public double SpeedStrain;
public double ApproachRate;
public double OverallDifficulty;
public int MaxCombo;
public OsuDifficultyAttributes(Mod[] mods, double starRating) public OsuDifficultyAttributes(Mod[] mods, double starRating)
: base(mods, 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) 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); OsuDifficultyBeatmap difficultyBeatmap = new OsuDifficultyBeatmap(beatmap.HitObjects.Cast<OsuHitObject>().ToList(), timeRate);
Skill[] skills = Skill[] skills =
{ {
@ -58,10 +61,21 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double speedRating = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier; double speedRating = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier;
double starRating = aimRating + speedRating + Math.Abs(aimRating - speedRating) / 2; 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) return new OsuDifficultyAttributes(mods, starRating)
{ {
AimStrain = aimRating, 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; 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 double accuracy;
private int scoreMaxCombo; private int scoreMaxCombo;
private int countGreat; private int countGreat;
@ -63,13 +53,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (mods.Any(m => !m.Ranked)) if (mods.Any(m => !m.Ranked))
return 0; 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. // 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 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("Aim", aimValue);
categoryRatings.Add("Speed", speedValue); categoryRatings.Add("Speed", speedValue);
categoryRatings.Add("Accuracy", accuracyValue); categoryRatings.Add("Accuracy", accuracyValue);
categoryRatings.Add("OD", realOverallDifficulty); categoryRatings.Add("OD", Attributes.OverallDifficulty);
categoryRatings.Add("AR", realApproachRate); categoryRatings.Add("AR", Attributes.ApproachRate);
categoryRatings.Add("Max Combo", beatmapMaxCombo); 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); aimValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8f) / Math.Pow(beatmapMaxCombo, 0.8f), 1.0f);
double approachRateFactor = 1.0f; double approachRateFactor = 1.0f;
if (realApproachRate > 10.33f) if (Attributes.ApproachRate > 10.33f)
approachRateFactor += 0.45f * (realApproachRate - 10.33f); approachRateFactor += 0.45f * (Attributes.ApproachRate - 10.33f);
else if (realApproachRate < 8.0f) else if (Attributes.ApproachRate < 8.0f)
{ {
// HD is worth more with lower ar! // HD is worth more with lower ar!
if (mods.Any(h => h is OsuModHidden)) if (mods.Any(h => h is OsuModHidden))
approachRateFactor += 0.02f * (8.0f - realApproachRate); approachRateFactor += 0.02f * (8.0f - Attributes.ApproachRate);
else else
approachRateFactor += 0.01f * (8.0f - realApproachRate); approachRateFactor += 0.01f * (8.0f - Attributes.ApproachRate);
} }
aimValue *= approachRateFactor; 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. // 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)) 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)) if (mods.Any(h => h is OsuModFlashlight))
{ {
@ -146,7 +129,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
// Scale the aim value with accuracy _slightly_ // Scale the aim value with accuracy _slightly_
aimValue *= 0.5f + accuracy / 2.0f; aimValue *= 0.5f + accuracy / 2.0f;
// It is important to also consider accuracy difficulty when doing that // 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; return aimValue;
} }
@ -172,7 +155,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
// Scale the speed value with accuracy _slightly_ // Scale the speed value with accuracy _slightly_
speedValue *= 0.5f + accuracy / 2.0f; speedValue *= 0.5f + accuracy / 2.0f;
// It is important to also consider accuracy difficulty when doing that // 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; return speedValue;
} }
@ -194,7 +177,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
// Lots of arbitrary values from testing. // Lots of arbitrary values from testing.
// Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution // 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 // 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)); accuracyValue *= Math.Min(1.15f, Math.Pow(amountHitObjectsWithAccuracy / 1000.0f, 0.3f));

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> new ReplayState<OsuAction>
{ {
Mouse = new ReplayMouseState(GamefieldToScreenSpace(Position ?? Vector2.Zero)),
PressedActions = CurrentFrame.Actions PressedActions = CurrentFrame.Actions
} }
}; };

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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods; 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) protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double timeRate)
{ {
if (!beatmap.HitObjects.Any())
return new TaikoDifficultyAttributes(mods, 0);
var difficultyHitObjects = new List<TaikoHitObjectDifficulty>(); var difficultyHitObjects = new List<TaikoHitObjectDifficulty>();
foreach (var hitObject in beatmap.HitObjects) foreach (var hitObject in beatmap.HitObjects)
@ -47,7 +51,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
double starRating = calculateDifficulty(difficultyHitObjects, timeRate) * star_scaling_factor; 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) 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.Difficulty;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Rulesets.Taiko.Difficulty namespace osu.Game.Rulesets.Taiko.Difficulty
{ {
public class TaikoPerformanceCalculator : PerformanceCalculator public class TaikoPerformanceCalculator : PerformanceCalculator
{ {
private readonly int beatmapMaxCombo; protected new TaikoDifficultyAttributes Attributes => (TaikoDifficultyAttributes)base.Attributes;
private Mod[] mods; private Mod[] mods;
private int countGreat; private int countGreat;
@ -25,7 +24,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
public TaikoPerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, Score score) public TaikoPerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, Score score)
: base(ruleset, beatmap, score) : base(ruleset, beatmap, score)
{ {
beatmapMaxCombo = Beatmap.HitObjects.Count(h => h is Hit);
} }
public override double Calculate(Dictionary<string, double> categoryDifficulty = null) 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); strainValue *= Math.Pow(0.985, countMiss);
// Combo scaling // Combo scaling
if (beatmapMaxCombo > 0) if (Attributes.MaxCombo > 0)
strainValue *= Math.Min(Math.Pow(Score.MaxCombo, 0.5) / Math.Pow(beatmapMaxCombo, 0.5), 1.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)) if (mods.Any(m => m is ModHidden))
strainValue *= 1.025; strainValue *= 1.025;
@ -94,14 +92,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
private double computeAccuracyValue() private double computeAccuracyValue()
{ {
// Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be remoevd in the future if (Attributes.GreatHitWindow <= 0)
double hitWindowGreat = (int)(Beatmap.HitObjects.First().HitWindows.Great / 2) / TimeRate;
if (hitWindowGreat <= 0)
return 0; return 0;
// Lots of arbitrary values from testing. // Lots of arbitrary values from testing.
// Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution // 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 // 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)); 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) switch (State.Value)
{ {
case ArmedState.Idle: case ArmedState.Idle:
SecondHitAllowed = false;
validKeyPressed = false;
UnproxyContent(); UnproxyContent();
this.Delay(HitObject.HitWindows.HalfWindowFor(HitResult.Miss)).Expire(); this.Delay(HitObject.HitWindows.HalfWindowFor(HitResult.Miss)).Expire();
break; break;
@ -95,8 +98,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
break; break;
case ArmedState.Hit: case ArmedState.Hit:
// If we're far enough away from the left stage, we should bring outselves in front of it // 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; var flash = circlePiece?.FlashBox;
if (flash != null) if (flash != null)

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Linq; using System.Linq;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Judgements; using osu.Game.Rulesets.Taiko.Judgements;
@ -46,6 +47,19 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
AddJudgement(new TaikoStrongHitJudgement { Result = HitResult.Great }); 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) public override bool OnReleased(TaikoAction action)
{ {
if (action == firstHitAction) if (action == firstHitAction)

View File

@ -2,7 +2,6 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System; using System;
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -20,6 +19,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{ {
public class DrawableSwell : DrawableTaikoHitObject<Swell> 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_thick_border = 1.4f;
private const float target_ring_thin_border = 1f; private const float target_ring_thin_border = 1f;
private const float target_ring_scale = 5f; 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 targetRing;
private readonly CircularContainer expandingRing; private readonly CircularContainer expandingRing;
/// <summary>
/// The amount of times the user has hit this swell.
/// </summary>
private int userHits;
private readonly SwellSymbolPiece symbol; private readonly SwellSymbolPiece symbol;
public DrawableSwell(Swell swell) public DrawableSwell(Swell swell)
@ -129,9 +128,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{ {
if (userTriggered) if (userTriggered)
{ {
userHits++; AddJudgement(new TaikoIntermediateSwellJudgement());
var completion = (float)userHits / HitObject.RequiredHits; var completion = (float)Judgements.Count / HitObject.RequiredHits;
expandingRing expandingRing
.FadeTo(expandingRing.Alpha + MathHelper.Clamp(completion / 16, 0.1f, 0.6f), 50) .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); 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 }); AddJudgement(new TaikoJudgement { Result = HitResult.Great });
} }
else else
@ -151,7 +150,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
return; return;
//TODO: THIS IS SHIT AND CAN'T EXIST POST-TAIKO WORLD CUP //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.Good }
: new TaikoJudgement { Result = HitResult.Miss }); : new TaikoJudgement { Result = HitResult.Miss });
} }
@ -162,23 +161,22 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
const float preempt = 100; const float preempt = 100;
const float out_transition_time = 300; 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) switch (state)
{ {
case ArmedState.Idle: case ArmedState.Idle:
UnproxyContent(); UnproxyContent();
expandingRing.FadeTo(0);
using (BeginAbsoluteSequence(HitObject.StartTime - preempt, true))
targetRing.ScaleTo(target_ring_scale, preempt * 4, Easing.OutQuint);
break; break;
case ArmedState.Miss:
case ArmedState.Hit: 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; break;
} }
Expire();
} }
protected override void Update() 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(); 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

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

View File

@ -1,8 +1,7 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>. // Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System; using NUnit.Framework;
using System.Collections.Generic;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -10,9 +9,6 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; 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.Rulesets.Osu.Objects;
using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Beatmaps;
using OpenTK; using OpenTK;
@ -20,10 +16,9 @@ using OpenTK.Graphics;
namespace osu.Game.Tests.Visual namespace osu.Game.Tests.Visual
{ {
[TestFixture]
public class TestCaseEditorSeekSnapping : EditorClockTestCase public class TestCaseEditorSeekSnapping : EditorClockTestCase
{ {
public override IReadOnlyList<Type> RequiredTypes => new[] { typeof(HitObjectComposer) };
public TestCaseEditorSeekSnapping() public TestCaseEditorSeekSnapping()
{ {
BeatDivisor.Value = 4; BeatDivisor.Value = 4;
@ -56,22 +51,13 @@ namespace osu.Game.Tests.Visual
Beatmap.Value = new TestWorkingBeatmap(testBeatmap); Beatmap.Value = new TestWorkingBeatmap(testBeatmap);
Child = new TimingPointVisualiser(testBeatmap, 5000) { Clock = Clock }; Child = new TimingPointVisualiser(testBeatmap, 5000) { Clock = Clock };
testSeekNoSnapping();
testSeekSnappingOnBeat();
testSeekSnappingInBetweenBeat();
testSeekForwardNoSnapping();
testSeekForwardSnappingOnBeat();
testSeekForwardSnappingFromInBetweenBeat();
testSeekBackwardSnappingOnBeat();
testSeekBackwardSnappingFromInBetweenBeat();
testSeekingWithFloatingPointBeatLength();
} }
/// <summary> /// <summary>
/// Tests whether time is correctly seeked without snapping. /// Tests whether time is correctly seeked without snapping.
/// </summary> /// </summary>
private void testSeekNoSnapping() [Test]
public void TestSeekNoSnapping()
{ {
reset(); reset();
@ -94,7 +80,8 @@ namespace osu.Game.Tests.Visual
/// Tests whether seeking to exact beat times puts us on the beat time. /// Tests whether seeking to exact beat times puts us on the beat time.
/// These are the white/yellow ticks on the graph. /// These are the white/yellow ticks on the graph.
/// </summary> /// </summary>
private void testSeekSnappingOnBeat() [Test]
public void TestSeekSnappingOnBeat()
{ {
reset(); reset();
@ -117,9 +104,9 @@ namespace osu.Game.Tests.Visual
/// <summary> /// <summary>
/// Tests whether seeking to somewhere in the middle between beats puts us on the expected beats. /// 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. /// 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> /// </summary>
private void testSeekSnappingInBetweenBeat() [Test]
public void TestSeekSnappingInBetweenBeat()
{ {
reset(); reset();
@ -140,7 +127,8 @@ namespace osu.Game.Tests.Visual
/// <summary> /// <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). /// 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> /// </summary>
private void testSeekForwardNoSnapping() [Test]
public void TestSeekForwardNoSnapping()
{ {
reset(); reset();
@ -159,7 +147,8 @@ namespace osu.Game.Tests.Visual
/// <summary> /// <summary>
/// Tests that when seeking forward with beat snapping, all beats are snapped to and timing points are never skipped. /// Tests that when seeking forward with beat snapping, all beats are snapped to and timing points are never skipped.
/// </summary> /// </summary>
private void testSeekForwardSnappingOnBeat() [Test]
public void TestSeekForwardSnappingOnBeat()
{ {
reset(); 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. /// 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. /// This will also test being extremely close to the next beat/timing point, to ensure rounding is not an issue.
/// </summary> /// </summary>
private void testSeekForwardSnappingFromInBetweenBeat() [Test]
public void TestSeekForwardSnappingFromInBetweenBeat()
{ {
reset(); reset();
@ -214,21 +204,20 @@ namespace osu.Game.Tests.Visual
/// <summary> /// <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). /// 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> /// </summary>
private void testSeekBackwardNoSnapping() [Test]
public void TestSeekBackwardNoSnapping()
{ {
reset(); reset();
AddStep("Seek(450)", () => Clock.Seek(450)); AddStep("Seek(450)", () => Clock.Seek(450));
AddStep("SeekBackward", () => Clock.SeekBackward()); AddStep("SeekBackward", () => Clock.SeekBackward());
AddAssert("Time = 425", () => Clock.CurrentTime == 425); AddAssert("Time = 400", () => Clock.CurrentTime == 400);
AddStep("SeekBackward", () => Clock.SeekBackward()); AddStep("SeekBackward", () => Clock.SeekBackward());
AddAssert("Time = 375", () => Clock.CurrentTime == 375); AddAssert("Time = 350", () => Clock.CurrentTime == 350);
AddStep("SeekBackward", () => Clock.SeekBackward()); AddStep("SeekBackward", () => Clock.SeekBackward());
AddAssert("Time = 325", () => Clock.CurrentTime == 325); AddAssert("Time = 150", () => Clock.CurrentTime == 150);
AddStep("SeekBackward", () => Clock.SeekBackward()); AddStep("SeekBackward", () => Clock.SeekBackward());
AddAssert("Time = 125", () => Clock.CurrentTime == 125); AddAssert("Time = 50", () => Clock.CurrentTime == 50);
AddStep("SeekBackward", () => Clock.SeekBackward());
AddAssert("Time = 25", () => Clock.CurrentTime == 25);
AddStep("SeekBackward", () => Clock.SeekBackward()); AddStep("SeekBackward", () => Clock.SeekBackward());
AddAssert("Time = 0", () => Clock.CurrentTime == 0); AddAssert("Time = 0", () => Clock.CurrentTime == 0);
} }
@ -236,7 +225,8 @@ namespace osu.Game.Tests.Visual
/// <summary> /// <summary>
/// Tests that when seeking backward with beat snapping, all beats are snapped to and timing points are never skipped. /// Tests that when seeking backward with beat snapping, all beats are snapped to and timing points are never skipped.
/// </summary> /// </summary>
private void testSeekBackwardSnappingOnBeat() [Test]
public void TestSeekBackwardSnappingOnBeat()
{ {
reset(); 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. /// 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. /// This will also test being extremely close to the previous beat/timing point, to ensure rounding is not an issue.
/// </summary> /// </summary>
private void testSeekBackwardSnappingFromInBetweenBeat() [Test]
public void TestSeekBackwardSnappingFromInBetweenBeat()
{ {
reset(); reset();
@ -280,7 +271,8 @@ namespace osu.Game.Tests.Visual
/// <summary> /// <summary>
/// Tests that there are no rounding issues when snapping to beats within a timing point with a floating-point beatlength. /// Tests that there are no rounding issues when snapping to beats within a timing point with a floating-point beatlength.
/// </summary> /// </summary>
private void testSeekingWithFloatingPointBeatLength() [Test]
public void TestSeekingWithFloatingPointBeatLength()
{ {
reset(); reset();
@ -288,7 +280,7 @@ namespace osu.Game.Tests.Visual
AddStep("Seek(0)", () => Clock.Seek(0)); AddStep("Seek(0)", () => Clock.Seek(0));
for (int i = 0; i < 20; i++) for (int i = 0; i < 9; i++)
{ {
AddStep("SeekForward, Snap", () => AddStep("SeekForward, Snap", () =>
{ {
@ -298,7 +290,7 @@ namespace osu.Game.Tests.Visual
AddAssert("Time > lastTime", () => Clock.CurrentTime > lastTime); AddAssert("Time > lastTime", () => Clock.CurrentTime > lastTime);
} }
for (int i = 0; i < 20; i++) for (int i = 0; i < 9; i++)
{ {
AddStep("SeekBackward, Snap", () => AddStep("SeekBackward, Snap", () =>
{ {
@ -316,16 +308,6 @@ namespace osu.Game.Tests.Visual
AddStep("Reset", () => Clock.Seek(0)); 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 class TimingPointVisualiser : CompositeDrawable
{ {
private readonly double length; private readonly double length;

View File

@ -167,6 +167,7 @@ namespace osu.Game.Tests.Visual
AddStep(@"set rooms", () => lounge.Rooms = rooms); AddStep(@"set rooms", () => lounge.Rooms = rooms);
selectAssert(1); selectAssert(1);
AddStep(@"open room 1", () => clickRoom(1)); AddStep(@"open room 1", () => clickRoom(1));
AddUntilStep(() => lounge.ChildScreen?.IsCurrentScreen == true, "wait until room current");
AddStep(@"make lounge current", lounge.MakeCurrent); AddStep(@"make lounge current", lounge.MakeCurrent);
filterAssert(@"THE FINAL", LoungeTab.Public, 1); filterAssert(@"THE FINAL", LoungeTab.Public, 1);
filterAssert(string.Empty, LoungeTab.Public, 2); filterAssert(string.Empty, LoungeTab.Public, 2);

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 private class TestPlayfield : ScrollingPlayfield
{ {
public readonly ScrollingDirection Direction; public new readonly ScrollingDirection Direction;
public TestPlayfield(ScrollingDirection direction) public TestPlayfield(ScrollingDirection direction)
: base(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) if (!force && beatmap.OnlineBeatmapID != null && beatmap.BeatmapSet.OnlineBeatmapSetID != null)
return true; return true;
if (api.State != APIState.Online)
return false;
Logger.Log("Attempting online lookup for IDs...", LoggingTarget.Database); Logger.Log("Attempting online lookup for IDs...", LoggingTarget.Database);
try try

View File

@ -116,9 +116,6 @@ namespace osu.Game.Beatmaps
mod.ApplyToDifficulty(converted.BeatmapInfo.BaseDifficulty); 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 // Compute default values for hitobjects, including creating nested hitobjects in-case they're needed
foreach (var obj in converted.HitObjects) foreach (var obj in converted.HitObjects)
obj.ApplyDefaults(converted.ControlPointInfo, converted.BeatmapInfo.BaseDifficulty); obj.ApplyDefaults(converted.ControlPointInfo, converted.BeatmapInfo.BaseDifficulty);
@ -127,6 +124,9 @@ namespace osu.Game.Beatmaps
foreach (var obj in converted.HitObjects) foreach (var obj in converted.HitObjects)
mod.ApplyToHitObject(obj); mod.ApplyToHitObject(obj);
// Post-process
rulesetInstance.CreateBeatmapProcessor(converted)?.PostProcess();
return converted; return converted;
} }

View File

@ -8,22 +8,34 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Input; using osu.Framework.Input;
using OpenTK; using OpenTK;
using osu.Framework.Configuration; using osu.Framework.Configuration;
using osu.Game.Audio;
using osu.Game.Overlays; using osu.Game.Overlays;
namespace osu.Game.Graphics.Containers namespace osu.Game.Graphics.Containers
{ {
public class OsuFocusedOverlayContainer : FocusedOverlayContainer public class OsuFocusedOverlayContainer : FocusedOverlayContainer, IPreviewTrackOwner
{ {
private SampleChannel samplePopIn; private SampleChannel samplePopIn;
private SampleChannel samplePopOut; private SampleChannel samplePopOut;
protected virtual bool PlaySamplesOnStateChange => true; protected virtual bool PlaySamplesOnStateChange => true;
private PreviewTrackManager previewTrackManager;
protected readonly Bindable<OverlayActivation> OverlayActivationMode = new Bindable<OverlayActivation>(OverlayActivation.All); protected readonly Bindable<OverlayActivation> OverlayActivationMode = new Bindable<OverlayActivation>(OverlayActivation.All);
[BackgroundDependencyLoader(true)] protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent)
private void load(OsuGame osuGame, AudioManager audio)
{ {
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) if (osuGame != null)
OverlayActivationMode.BindTo(osuGame.OverlayActivationMode); OverlayActivationMode.BindTo(osuGame.OverlayActivationMode);
@ -70,5 +82,11 @@ namespace osu.Game.Graphics.Containers
break; break;
} }
} }
protected override void PopOut()
{
base.PopOut();
previewTrackManager.StopAnyPlaying(this);
}
} }
} }

View File

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

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. // 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 // Without this, we will end up circulating this Connecting loop multiple times and queueing up many web requests
// before actually going online. // before actually going online.
while (State != APIState.Online) while (State > APIState.Offline && State < APIState.Online)
Thread.Sleep(500); Thread.Sleep(500);
break; break;
@ -158,7 +158,6 @@ namespace osu.Game.Online.API
if (authentication.RequestAccessToken() == null) if (authentication.RequestAccessToken() == null)
{ {
Logout(false); Logout(false);
State = APIState.Offline;
continue; 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); 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) switch (statusCode)
{ {
case HttpStatusCode.Unauthorized: case HttpStatusCode.Unauthorized:
@ -292,6 +299,7 @@ namespace osu.Game.Online.API
password = null; password = null;
authentication.Clear(); authentication.Clear();
LocalUser.Value = createGuestUser(); LocalUser.Value = createGuestUser();
State = APIState.Offline;
} }
private static User createGuestUser() => new User private static User createGuestUser() => new User

View File

@ -101,6 +101,8 @@ namespace osu.Game
public OsuGame(string[] args = null) public OsuGame(string[] args = null)
{ {
this.args = args; this.args = args;
forwardLoggedErrorsToNotifications();
} }
public void ToggleSettings() => settings.ToggleVisibility(); public void ToggleSettings() => settings.ToggleVisibility();
@ -305,8 +307,6 @@ namespace osu.Game
Depth = -6, Depth = -6,
}, overlayContent.Add); }, overlayContent.Add);
forwardLoggedErrorsToNotifications();
dependencies.Cache(settings); dependencies.Cache(settings);
dependencies.Cache(onscreenDisplay); dependencies.Cache(onscreenDisplay);
dependencies.Cache(social); dependencies.Cache(social);
@ -394,31 +394,40 @@ namespace osu.Game
private void forwardLoggedErrorsToNotifications() private void forwardLoggedErrorsToNotifications()
{ {
int recentErrorCount = 0; int recentLogCount = 0;
const double debounce = 5000; const double debounce = 5000;
Logger.NewEntry += entry => Logger.NewEntry += entry =>
{ {
if (entry.Level < LogLevel.Error || entry.Target == null) return; if (entry.Level < LogLevel.Important || entry.Target == null) return;
if (recentErrorCount < 2) const int short_term_display_limit = 3;
if (recentLogCount < short_term_display_limit)
{ {
notifications.Post(new SimpleNotification Schedule(() => notifications.Post(new SimpleNotification
{ {
Icon = FontAwesome.fa_bomb, Icon = entry.Level == LogLevel.Important ? FontAwesome.fa_exclamation_circle : FontAwesome.fa_bomb,
Text = (recentErrorCount == 0 ? entry.Message : "Subsequent errors occurred and have been logged.") + "\nClick to view log files.", Text = entry.Message,
}));
}
else if (recentLogCount == short_term_display_limit)
{
Schedule(() => notifications.Post(new SimpleNotification
{
Icon = FontAwesome.fa_ellipsis_h,
Text = "Subsequent messages have been logged. Click to view log files.",
Activated = () => Activated = () =>
{ {
Host.Storage.GetStorageForDirectory("logs").OpenInNativeExplorer(); Host.Storage.GetStorageForDirectory("logs").OpenInNativeExplorer();
return true; return true;
} }
}); }));
} }
Interlocked.Increment(ref recentErrorCount); Interlocked.Increment(ref recentLogCount);
Scheduler.AddDelayed(() => Interlocked.Decrement(ref recentLogCount), debounce);
Scheduler.AddDelayed(() => Interlocked.Decrement(ref recentErrorCount), debounce);
}; };
} }

View File

@ -21,6 +21,7 @@ using osu.Game.Online.API;
using osu.Framework.Graphics.Performance; using osu.Framework.Graphics.Performance;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Game.Audio;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Graphics.Textures; using osu.Game.Graphics.Textures;
using osu.Game.Input; using osu.Game.Input;
@ -187,6 +188,10 @@ namespace osu.Game
KeyBindingStore.Register(globalBinding); KeyBindingStore.Register(globalBinding);
dependencies.Cache(globalBinding); dependencies.Cache(globalBinding);
PreviewTrackManager previewTrackManager;
dependencies.Cache(previewTrackManager = new PreviewTrackManager());
Add(previewTrackManager);
} }
protected override void LoadComplete() protected override void LoadComplete()

View File

@ -2,13 +2,13 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Configuration; using osu.Framework.Configuration;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
@ -25,7 +25,7 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons
private readonly Box bg, progress; private readonly Box bg, progress;
private readonly PlayButton playButton; private readonly PlayButton playButton;
private Track preview => playButton.Preview; private PreviewTrack preview => playButton.Preview;
public Bindable<bool> Playing => playButton.Playing; public Bindable<bool> Playing => playButton.Playing;
public BeatmapSetInfo BeatmapSet public BeatmapSetInfo BeatmapSet
@ -66,7 +66,7 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons
}, },
}; };
Action = () => Playing.Value = !Playing.Value; Action = () => playButton.TriggerOnClick();
Playing.ValueChanged += newValue => progress.FadeTo(newValue ? 1 : 0, 100); Playing.ValueChanged += newValue => progress.FadeTo(newValue ? 1 : 0, 100);
} }
@ -89,12 +89,6 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons
progress.Width = 0; progress.Width = 0;
} }
protected override void Dispose(bool isDisposing)
{
Playing.Value = false;
base.Dispose(isDisposing);
}
protected override bool OnHover(InputState state) protected override bool OnHover(InputState state)
{ {
bg.FadeColour(Color4.Black.Opacity(0.5f), 100); bg.FadeColour(Color4.Black.Opacity(0.5f), 100);

View File

@ -102,8 +102,6 @@ namespace osu.Game.Overlays.BeatmapSet
updateDisplay(); updateDisplay();
} }
public void StopPreview() => preview.Playing.Value = false;
private class DetailBox : Container private class DetailBox : Container
{ {
private readonly Container content; private readonly Container content;

View File

@ -1,9 +1,8 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>. // Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using OpenTK;
using OpenTK.Graphics;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -15,9 +14,10 @@ using osu.Game.Graphics.Containers;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Overlays.BeatmapSet; using osu.Game.Overlays.BeatmapSet;
using osu.Game.Rulesets;
using osu.Game.Overlays.BeatmapSet.Scores; using osu.Game.Overlays.BeatmapSet.Scores;
using System.Linq; using osu.Game.Rulesets;
using OpenTK;
using OpenTK.Graphics;
namespace osu.Game.Overlays namespace osu.Game.Overlays
{ {
@ -124,8 +124,6 @@ namespace osu.Game.Overlays
protected override void PopOut() protected override void PopOut()
{ {
base.PopOut(); base.PopOut();
header.Details.StopPreview();
FadeEdgeEffectTo(0, WaveContainer.DISAPPEAR_DURATION, Easing.Out).OnComplete(_ => BeatmapSet = null); FadeEdgeEffectTo(0, WaveContainer.DISAPPEAR_DURATION, Easing.Out).OnComplete(_ => BeatmapSet = null);
} }

View File

@ -4,22 +4,22 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using OpenTK;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input;
using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables; using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using OpenTK.Graphics;
using osu.Framework.Input;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Framework.Configuration; using OpenTK;
using osu.Framework.Audio.Track; using OpenTK.Graphics;
namespace osu.Game.Overlays.Direct namespace osu.Game.Overlays.Direct
{ {
@ -35,7 +35,7 @@ namespace osu.Game.Overlays.Direct
private BeatmapManager beatmaps; private BeatmapManager beatmaps;
private BeatmapSetOverlay beatmapSetOverlay; private BeatmapSetOverlay beatmapSetOverlay;
public Track Preview => PlayButton.Preview; public PreviewTrack Preview => PlayButton.Preview;
public Bindable<bool> PreviewPlaying => PlayButton.Playing; public Bindable<bool> PreviewPlaying => PlayButton.Playing;
protected abstract PlayButton PlayButton { get; } protected abstract PlayButton PlayButton { get; }
protected abstract Box PreviewBar { get; } protected abstract Box PreviewBar { get; }
@ -113,7 +113,7 @@ namespace osu.Game.Overlays.Direct
{ {
base.Update(); base.Update();
if (PreviewPlaying && Preview != null && Preview.IsLoaded) if (PreviewPlaying && Preview != null && Preview.TrackLoaded)
{ {
PreviewBar.Width = (float)(Preview.CurrentTime / Preview.Length); PreviewBar.Width = (float)(Preview.CurrentTime / Preview.Length);
} }
@ -141,7 +141,6 @@ namespace osu.Game.Overlays.Direct
protected override bool OnClick(InputState state) protected override bool OnClick(InputState state)
{ {
ShowInformation(); ShowInformation();
PreviewPlaying.Value = false;
return true; return true;
} }

View File

@ -1,25 +1,23 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>. // Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using OpenTK.Graphics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Configuration; using osu.Framework.Configuration;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.IO.Stores; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using OpenTK.Graphics;
namespace osu.Game.Overlays.Direct namespace osu.Game.Overlays.Direct
{ {
public class PlayButton : Container public class PlayButton : Container
{ {
public readonly Bindable<bool> Playing = new Bindable<bool>(); public readonly BindableBool Playing = new BindableBool();
public Track Preview { get; private set; } public PreviewTrack Preview { get; private set; }
private BeatmapSetInfo beatmapSet; private BeatmapSetInfo beatmapSet;
@ -31,9 +29,11 @@ namespace osu.Game.Overlays.Direct
if (value == beatmapSet) return; if (value == beatmapSet) return;
beatmapSet = value; beatmapSet = value;
Playing.Value = false; Preview?.Stop();
trackLoader = null; Preview?.Expire();
Preview = null; Preview = null;
Playing.Value = false;
} }
} }
@ -41,8 +41,6 @@ namespace osu.Game.Overlays.Direct
private readonly SpriteIcon icon; private readonly SpriteIcon icon;
private readonly LoadingAnimation loadingAnimation; private readonly LoadingAnimation loadingAnimation;
private readonly BindableDouble muteBindable = new BindableDouble();
private const float transition_duration = 500; private const float transition_duration = 500;
private bool loading private bool loading
@ -50,15 +48,9 @@ namespace osu.Game.Overlays.Direct
set set
{ {
if (value) if (value)
{
loadingAnimation.Show(); loadingAnimation.Show();
icon.FadeOut(transition_duration * 5, Easing.OutQuint);
}
else else
{
loadingAnimation.Hide(); loadingAnimation.Hide();
icon.FadeIn(transition_duration, Easing.OutQuint);
}
} }
} }
@ -78,19 +70,22 @@ namespace osu.Game.Overlays.Direct
loadingAnimation = new LoadingAnimation(), loadingAnimation = new LoadingAnimation(),
}); });
Playing.ValueChanged += updatePreviewTrack; Playing.ValueChanged += playingStateChanged;
} }
private PreviewTrackManager previewTrackManager;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colour, AudioManager audio) private void load(OsuColour colour, PreviewTrackManager previewTrackManager)
{ {
this.previewTrackManager = previewTrackManager;
hoverColour = colour.Yellow; hoverColour = colour.Yellow;
this.audio = audio;
} }
protected override bool OnClick(InputState state) protected override bool OnClick(InputState state)
{ {
Playing.Value = !Playing.Value; Playing.Toggle();
return true; return true;
} }
@ -107,44 +102,44 @@ namespace osu.Game.Overlays.Direct
base.OnHoverLost(state); base.OnHoverLost(state);
} }
protected override void Update() private void playingStateChanged(bool playing)
{ {
base.Update();
if (Preview?.HasCompleted ?? false)
{
Playing.Value = false;
Preview = null;
}
}
private void updatePreviewTrack(bool playing)
{
if (playing && BeatmapSet == null)
{
Playing.Value = false;
return;
}
icon.Icon = playing ? FontAwesome.fa_stop : FontAwesome.fa_play; icon.Icon = playing ? FontAwesome.fa_stop : FontAwesome.fa_play;
icon.FadeColour(playing || IsHovered ? hoverColour : Color4.White, 120, Easing.InOutQuint); icon.FadeColour(playing || IsHovered ? hoverColour : Color4.White, 120, Easing.InOutQuint);
if (playing) if (playing)
{ {
if (Preview == null) if (BeatmapSet == null)
{ {
beginAudioLoad(); Playing.Value = false;
return; return;
} }
Preview.Restart(); if (Preview != null)
{
Preview.Start();
return;
}
audio.Track.AddAdjustment(AdjustableProperty.Volume, muteBindable); loading = true;
LoadComponentAsync(Preview = previewTrackManager.Get(beatmapSet), preview =>
{
// beatmapset may have changed.
if (Preview != preview)
return;
AddInternal(preview);
loading = false;
preview.Stopped += () => Playing.Value = false;
// user may have changed their mind.
if (Playing)
preview.Start();
});
} }
else else
{ {
audio.Track.RemoveAdjustment(AdjustableProperty.Volume, muteBindable);
Preview?.Stop(); Preview?.Stop();
loading = false; loading = false;
} }
@ -155,64 +150,5 @@ namespace osu.Game.Overlays.Direct
base.Dispose(isDisposing); base.Dispose(isDisposing);
Playing.Value = false; Playing.Value = false;
} }
private TrackLoader trackLoader;
private AudioManager audio;
private void beginAudioLoad()
{
if (trackLoader != null)
{
Preview = trackLoader.Preview;
Playing.TriggerChange();
return;
}
loading = true;
LoadComponentAsync(trackLoader = new TrackLoader($"https://b.ppy.sh/preview/{BeatmapSet.OnlineBeatmapSetID}.mp3"),
d =>
{
// We may have been replaced by another loader
if (trackLoader != d) return;
Preview = d?.Preview;
updatePreviewTrack(Playing);
loading = false;
Add(trackLoader);
});
}
private class TrackLoader : Drawable
{
private readonly string preview;
public Track Preview;
private TrackManager trackManager;
public TrackLoader(string preview)
{
this.preview = preview;
}
[BackgroundDependencyLoader]
private void load(AudioManager audio, FrameworkConfigManager config)
{
// create a local trackManager to bypass the mute we are applying above.
audio.AddItem(trackManager = new TrackManager(new OnlineStore()));
// add back the user's music volume setting (since we are no longer in the global TrackManager's hierarchy).
config.BindWith(FrameworkSetting.VolumeMusic, trackManager.Volume);
Preview = trackManager.Get(preview);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
trackManager?.Dispose();
}
}
} }
} }

View File

@ -4,12 +4,12 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using OpenTK;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Configuration; using osu.Framework.Configuration;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
@ -18,6 +18,7 @@ using osu.Game.Online.API.Requests;
using osu.Game.Overlays.Direct; using osu.Game.Overlays.Direct;
using osu.Game.Overlays.SearchableList; using osu.Game.Overlays.SearchableList;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using OpenTK;
using OpenTK.Graphics; using OpenTK.Graphics;
namespace osu.Game.Overlays namespace osu.Game.Overlays
@ -32,7 +33,6 @@ namespace osu.Game.Overlays
private readonly FillFlowContainer resultCountsContainer; private readonly FillFlowContainer resultCountsContainer;
private readonly OsuSpriteText resultCountsText; private readonly OsuSpriteText resultCountsText;
private FillFlowContainer<DirectPanel> panels; private FillFlowContainer<DirectPanel> panels;
private DirectPanel playing;
protected override Color4 BackgroundColour => OsuColour.FromHex(@"485e74"); protected override Color4 BackgroundColour => OsuColour.FromHex(@"485e74");
protected override Color4 TrianglesColourLight => OsuColour.FromHex(@"465b71"); protected override Color4 TrianglesColourLight => OsuColour.FromHex(@"465b71");
@ -176,10 +176,11 @@ namespace osu.Game.Overlays
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours, APIAccess api, RulesetStore rulesets) private void load(OsuColour colours, APIAccess api, RulesetStore rulesets, PreviewTrackManager previewTrackManager)
{ {
this.api = api; this.api = api;
this.rulesets = rulesets; this.rulesets = rulesets;
this.previewTrackManager = previewTrackManager;
resultCountsContainer.Colour = colours.Yellow; resultCountsContainer.Colour = colours.Yellow;
} }
@ -206,12 +207,6 @@ namespace osu.Game.Overlays
panels.FadeOut(200); panels.FadeOut(200);
panels.Expire(); panels.Expire();
panels = null; panels = null;
if (playing != null)
{
playing.PreviewPlaying.Value = false;
playing = null;
}
} }
if (BeatmapSets == null) return; if (BeatmapSets == null) return;
@ -242,17 +237,6 @@ namespace osu.Game.Overlays
{ {
if (panels != null) ScrollFlow.Remove(panels); if (panels != null) ScrollFlow.Remove(panels);
ScrollFlow.Add(panels = newPanels); ScrollFlow.Add(panels = newPanels);
foreach (DirectPanel panel in p.Children)
panel.PreviewPlaying.ValueChanged += newValue =>
{
if (newValue)
{
if (playing != null && playing != panel)
playing.PreviewPlaying.Value = false;
playing = panel;
}
};
}); });
} }
@ -261,6 +245,7 @@ namespace osu.Game.Overlays
private readonly Bindable<string> currentQuery = new Bindable<string>(); private readonly Bindable<string> currentQuery = new Bindable<string>();
private ScheduledDelegate queryChangedDebounce; private ScheduledDelegate queryChangedDebounce;
private PreviewTrackManager previewTrackManager;
private void updateSearch() private void updateSearch()
{ {
@ -277,6 +262,8 @@ namespace osu.Game.Overlays
if (Header.Tabs.Current.Value == DirectTab.Search && (Filter.Search.Text == string.Empty || currentQuery == string.Empty)) return; if (Header.Tabs.Current.Value == DirectTab.Search && (Filter.Search.Text == string.Empty || currentQuery == string.Empty)) return;
previewTrackManager.StopAnyPlaying(this);
getSetsRequest = new SearchBeatmapSetsRequest(currentQuery.Value ?? string.Empty, getSetsRequest = new SearchBeatmapSetsRequest(currentQuery.Value ?? string.Empty,
((FilterControl)Filter).Ruleset.Value, ((FilterControl)Filter).Ruleset.Value,
Filter.DisplayStyleControl.Dropdown.Current.Value, Filter.DisplayStyleControl.Dropdown.Current.Value,
@ -300,14 +287,6 @@ namespace osu.Game.Overlays
api.Queue(getSetsRequest); api.Queue(getSetsRequest);
} }
protected override void PopOut()
{
base.PopOut();
if (playing != null)
playing.PreviewPlaying.Value = false;
}
private int distinctCount(List<string> list) => list.Distinct().ToArray().Length; private int distinctCount(List<string> list) => list.Distinct().ToArray().Length;
public class ResultCounts public class ResultCounts

View File

@ -186,7 +186,7 @@ namespace osu.Game.Overlays.KeyBinding
{ {
if (bindTarget.IsHovered) if (bindTarget.IsHovered)
{ {
bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(state)); bindTarget.UpdateKeyCombination(new KeyCombination(KeyCombination.FromInputState(state).Keys.Append(state.Mouse.ScrollDelta.Y > 0 ? InputKey.MouseWheelUp : InputKey.MouseWheelDown)));
finalise(); finalise();
return true; return true;
} }

View File

@ -59,7 +59,7 @@ namespace osu.Game.Overlays.Notifications
} }
}); });
Content.Add(textDrawable = new OsuTextFlowContainer(t => t.TextSize = 16) Content.Add(textDrawable = new OsuTextFlowContainer(t => t.TextSize = 14)
{ {
Colour = OsuColour.Gray(128), Colour = OsuColour.Gray(128),
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,

View File

@ -1,14 +1,13 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>. // Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System; using System.Linq;
using OpenTK;
using osu.Framework.Configuration; using osu.Framework.Configuration;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Overlays.Direct; using osu.Game.Overlays.Direct;
using osu.Game.Users; using osu.Game.Users;
using System.Linq; using OpenTK;
namespace osu.Game.Overlays.Profile.Sections.Beatmaps namespace osu.Game.Overlays.Profile.Sections.Beatmaps
{ {
@ -18,10 +17,6 @@ namespace osu.Game.Overlays.Profile.Sections.Beatmaps
private readonly BeatmapSetType type; private readonly BeatmapSetType type;
private DirectPanel currentlyPlaying;
public event Action<PaginatedBeatmapContainer> BeganPlayingPreview;
public PaginatedBeatmapContainer(BeatmapSetType type, Bindable<User> user, string header, string missing = "None... yet.") public PaginatedBeatmapContainer(BeatmapSetType type, Bindable<User> user, string header, string missing = "None... yet.")
: base(user, header, missing) : base(user, header, missing)
{ {
@ -56,28 +51,10 @@ namespace osu.Game.Overlays.Profile.Sections.Beatmaps
var panel = new DirectGridPanel(s.ToBeatmapSet(Rulesets)); var panel = new DirectGridPanel(s.ToBeatmapSet(Rulesets));
ItemsContainer.Add(panel); ItemsContainer.Add(panel);
panel.PreviewPlaying.ValueChanged += isPlaying =>
{
StopPlayingPreview();
if (isPlaying)
{
BeganPlayingPreview?.Invoke(this);
currentlyPlaying = panel;
}
};
} }
}; };
Api.Queue(req); Api.Queue(req);
} }
public void StopPlayingPreview()
{
if (currentlyPlaying == null) return;
currentlyPlaying.PreviewPlaying.Value = false;
currentlyPlaying = null;
}
} }
} }

View File

@ -1,7 +1,6 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>. // Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Linq;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Overlays.Profile.Sections.Beatmaps; using osu.Game.Overlays.Profile.Sections.Beatmaps;
@ -22,15 +21,6 @@ namespace osu.Game.Overlays.Profile.Sections
new PaginatedBeatmapContainer(BeatmapSetType.Unranked, User, "Pending Beatmaps"), new PaginatedBeatmapContainer(BeatmapSetType.Unranked, User, "Pending Beatmaps"),
new PaginatedBeatmapContainer(BeatmapSetType.Graveyard, User, "Graveyarded Beatmaps"), new PaginatedBeatmapContainer(BeatmapSetType.Graveyard, User, "Graveyarded Beatmaps"),
}; };
foreach (var paginatedBeatmapContainer in Children.OfType<PaginatedBeatmapContainer>())
{
paginatedBeatmapContainer.BeganPlayingPreview += _ =>
{
foreach (var bc in Children.OfType<PaginatedBeatmapContainer>())
bc.StopPlayingPreview();
};
}
} }
} }
} }

View File

@ -2,8 +2,6 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Linq; using System.Linq;
using OpenTK;
using OpenTK.Graphics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -18,6 +16,8 @@ using osu.Game.Online.API.Requests;
using osu.Game.Overlays.Profile; using osu.Game.Overlays.Profile;
using osu.Game.Overlays.Profile.Sections; using osu.Game.Overlays.Profile.Sections;
using osu.Game.Users; using osu.Game.Users;
using OpenTK;
using OpenTK.Graphics;
namespace osu.Game.Overlays namespace osu.Game.Overlays
{ {

View File

@ -66,7 +66,7 @@ namespace osu.Game.Overlays.Volume
protected override bool OnHover(InputState state) protected override bool OnHover(InputState state)
{ {
this.TransformTo<MuteButton, SRGBColour>("BorderColour", hoveredColour, 500, Easing.OutQuint); this.TransformTo<MuteButton, SRGBColour>("BorderColour", hoveredColour, 500, Easing.OutQuint);
return true; return false;
} }
protected override void OnHoverLost(InputState state) protected override void OnHoverLost(InputState state)

View File

@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.MathUtils; using osu.Framework.MathUtils;
using osu.Game.Graphics; using osu.Game.Graphics;
@ -232,12 +233,13 @@ namespace osu.Game.Overlays.Volume
{ {
float amount = adjust_step * direction; float amount = adjust_step * direction;
var mouse = GetContainingInputManager().CurrentState.Mouse; // handle the case where the OnPressed action was actually a mouse wheel.
if (mouse.HasPreciseScroll) // this allows for precise wheel handling.
var state = GetContainingInputManager().CurrentState;
if (state.Mouse?.ScrollDelta.Y != 0)
{ {
float scrollDelta = mouse.ScrollDelta.Y; OnScroll(state);
if (scrollDelta != 0) return;
amount *= Math.Abs(scrollDelta / 10);
} }
Volume += amount; Volume += amount;
@ -260,6 +262,34 @@ namespace osu.Game.Overlays.Volume
return false; return false;
} }
// because volume precision is set to 0.01, this local is required to keep track of more precise adjustments and only apply when possible.
private double scrollAmount;
protected override bool OnScroll(InputState state)
{
scrollAmount += adjust_step * state.Mouse.ScrollDelta.Y * (state.Mouse.HasPreciseScroll ? 0.1f : 1);
if (Math.Abs(scrollAmount) < Bindable.Precision)
return true;
Volume += scrollAmount;
scrollAmount = 0;
return true;
}
public bool OnReleased(GlobalAction action) => false; public bool OnReleased(GlobalAction action) => false;
private const float transition_length = 500;
protected override bool OnHover(InputState state)
{
this.ScaleTo(1.04f, transition_length, Easing.OutExpo);
return false;
}
protected override void OnHoverLost(InputState state)
{
this.ScaleTo(1f, transition_length, Easing.OutExpo);
}
} }
} }

View File

@ -9,6 +9,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Input;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
@ -86,16 +87,10 @@ namespace osu.Game.Overlays
{ {
base.LoadComplete(); base.LoadComplete();
volumeMeterMaster.Bindable.ValueChanged += _ => settingChanged(); volumeMeterMaster.Bindable.ValueChanged += _ => Show();
volumeMeterEffect.Bindable.ValueChanged += _ => settingChanged(); volumeMeterEffect.Bindable.ValueChanged += _ => Show();
volumeMeterMusic.Bindable.ValueChanged += _ => settingChanged(); volumeMeterMusic.Bindable.ValueChanged += _ => Show();
muteButton.Current.ValueChanged += _ => settingChanged(); muteButton.Current.ValueChanged += _ => Show();
}
private void settingChanged()
{
Show();
schedulePopOut();
} }
public bool Adjust(GlobalAction action) public bool Adjust(GlobalAction action)
@ -127,6 +122,14 @@ namespace osu.Game.Overlays
private ScheduledDelegate popOutDelegate; private ScheduledDelegate popOutDelegate;
public override void Show()
{
if (State == Visibility.Visible)
schedulePopOut();
base.Show();
}
protected override void PopIn() protected override void PopIn()
{ {
ClearTransforms(); ClearTransforms();
@ -140,10 +143,33 @@ namespace osu.Game.Overlays
this.FadeOut(100); this.FadeOut(100);
} }
protected override bool OnMouseMove(InputState state)
{
// keep the scheduled event correctly timed as long as we have movement.
schedulePopOut();
return base.OnMouseMove(state);
}
protected override bool OnHover(InputState state)
{
schedulePopOut();
return true;
}
protected override void OnHoverLost(InputState state)
{
schedulePopOut();
base.OnHoverLost(state);
}
private void schedulePopOut() private void schedulePopOut()
{ {
popOutDelegate?.Cancel(); popOutDelegate?.Cancel();
this.Delay(1000).Schedule(Hide, out popOutDelegate); this.Delay(1000).Schedule(() =>
{
if (!IsHovered)
Hide();
}, out popOutDelegate);
} }
} }
} }

View File

@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Replays
return true; return true;
} }
public override List<InputState> GetPendingStates() => new List<InputState>(); public override List<IInput> GetPendingInputs() => new List<IInput>();
public bool AtLastFrame => currentFrameIndex == Frames.Count - 1; public bool AtLastFrame => currentFrameIndex == Frames.Count - 1;
public bool AtFirstFrame => currentFrameIndex == 0; public bool AtFirstFrame => currentFrameIndex == 0;
@ -119,7 +119,8 @@ namespace osu.Game.Rulesets.Replays
{ {
public ReplayKeyboardState(List<Key> keys) public ReplayKeyboardState(List<Key> keys)
{ {
Keys = keys; foreach (var key in keys)
Keys.Add(key);
} }
} }
} }

View File

@ -15,6 +15,7 @@ using osu.Game.Input.Bindings;
using osu.Game.Input.Handlers; using osu.Game.Input.Handlers;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using OpenTK.Input; using OpenTK.Input;
using static osu.Game.Input.Handlers.ReplayInputHandler;
namespace osu.Game.Rulesets.UI namespace osu.Game.Rulesets.UI
{ {
@ -29,26 +30,43 @@ namespace osu.Game.Rulesets.UI
} }
} }
protected override InputState CreateInitialState()
{
var state = base.CreateInitialState();
return new RulesetInputManagerInputState<T>
{
Mouse = state.Mouse,
Keyboard = state.Keyboard,
Joystick = state.Joystick,
LastReplayState = null
};
}
protected readonly KeyBindingContainer<T> KeyBindingContainer; protected readonly KeyBindingContainer<T> KeyBindingContainer;
protected override Container<Drawable> Content => KeyBindingContainer; protected override Container<Drawable> Content => KeyBindingContainer;
protected RulesetInputManager(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) protected RulesetInputManager(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique)
{ {
InternalChild = KeyBindingContainer = new RulesetKeyBindingContainer(ruleset, variant, unique); InternalChild = KeyBindingContainer = CreateKeyBindingContainer(ruleset, variant, unique);
} }
#region Action mapping (for replays) #region Action mapping (for replays)
private List<T> lastPressedActions = new List<T>(); private List<T> lastPressedActions = new List<T>();
protected override void HandleNewState(InputState state) public override void HandleCustomInput(InputState state, IInput input)
{ {
base.HandleNewState(state); if (!(input is ReplayState<T> replayState))
{
base.HandleCustomInput(state, input);
return;
}
var replayState = state as ReplayInputHandler.ReplayState<T>; if (state is RulesetInputManagerInputState<T> inputState)
{
if (replayState == null) return; inputState.LastReplayState = replayState;
}
// Here we handle states specifically coming from a replay source. // Here we handle states specifically coming from a replay source.
// These have extra action information rather than keyboard keys or mouse buttons. // These have extra action information rather than keyboard keys or mouse buttons.
@ -80,7 +98,7 @@ namespace osu.Game.Rulesets.UI
if (replayInputHandler != null) RemoveHandler(replayInputHandler); if (replayInputHandler != null) RemoveHandler(replayInputHandler);
replayInputHandler = value; replayInputHandler = value;
UseParentState = replayInputHandler == null; UseParentInput = replayInputHandler == null;
if (replayInputHandler != null) if (replayInputHandler != null)
AddHandler(replayInputHandler); AddHandler(replayInputHandler);
@ -123,7 +141,7 @@ namespace osu.Game.Rulesets.UI
protected override bool RequiresChildrenUpdate => base.RequiresChildrenUpdate && validState; protected override bool RequiresChildrenUpdate => base.RequiresChildrenUpdate && validState;
private bool isAttached => replayInputHandler != null && !UseParentState; private bool isAttached => replayInputHandler != null && !UseParentInput;
private const int max_catch_up_updates_per_frame = 50; private const int max_catch_up_updates_per_frame = 50;
@ -249,6 +267,9 @@ namespace osu.Game.Rulesets.UI
} }
#endregion #endregion
protected virtual RulesetKeyBindingContainer CreateKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique)
=> new RulesetKeyBindingContainer(ruleset, variant, unique);
} }
/// <summary> /// <summary>
@ -267,4 +288,10 @@ namespace osu.Game.Rulesets.UI
{ {
void Attach(KeyCounterCollection keyCounter); void Attach(KeyCounterCollection keyCounter);
} }
public class RulesetInputManagerInputState<T> : InputState
where T : struct
{
public ReplayState<T> LastReplayState;
}
} }

View File

@ -29,17 +29,16 @@ namespace osu.Game.Rulesets.UI.Scrolling
/// </summary> /// </summary>
protected readonly SortedList<MultiplierControlPoint> ControlPoints = new SortedList<MultiplierControlPoint>(); protected readonly SortedList<MultiplierControlPoint> ControlPoints = new SortedList<MultiplierControlPoint>();
private readonly ScrollingDirection direction; public readonly Bindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();
private Cached initialStateCache = new Cached(); private Cached initialStateCache = new Cached();
public ScrollingHitObjectContainer(ScrollingDirection direction) public ScrollingHitObjectContainer()
{ {
this.direction = direction;
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
TimeRange.ValueChanged += v => initialStateCache.Invalidate(); TimeRange.ValueChanged += _ => initialStateCache.Invalidate();
Direction.ValueChanged += _ => initialStateCache.Invalidate();
} }
private ISpeedChangeVisualiser speedChangeVisualiser; private ISpeedChangeVisualiser speedChangeVisualiser;
@ -100,7 +99,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
if (!initialStateCache.IsValid) if (!initialStateCache.IsValid)
{ {
speedChangeVisualiser.ComputeInitialStates(Objects, direction, TimeRange, DrawSize); speedChangeVisualiser.ComputeInitialStates(Objects, Direction, TimeRange, DrawSize);
initialStateCache.Validate(); initialStateCache.Validate();
} }
} }
@ -110,7 +109,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
base.UpdateAfterChildrenLife(); base.UpdateAfterChildrenLife();
// We need to calculate this as soon as possible after lifetimes so that hitobjects get the final say in their positions // We need to calculate this as soon as possible after lifetimes so that hitobjects get the final say in their positions
speedChangeVisualiser.UpdatePositions(AliveObjects, direction, Time.Current, TimeRange, DrawSize); speedChangeVisualiser.UpdatePositions(AliveObjects, Direction, Time.Current, TimeRange, DrawSize);
} }
} }
} }

View File

@ -54,7 +54,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
/// </summary> /// </summary>
public new ScrollingHitObjectContainer HitObjects => (ScrollingHitObjectContainer)base.HitObjects; public new ScrollingHitObjectContainer HitObjects => (ScrollingHitObjectContainer)base.HitObjects;
private readonly ScrollingDirection direction; protected readonly Bindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();
/// <summary> /// <summary>
/// Creates a new <see cref="ScrollingPlayfield"/>. /// Creates a new <see cref="ScrollingPlayfield"/>.
@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
protected ScrollingPlayfield(ScrollingDirection direction, float? customWidth = null, float? customHeight = null) protected ScrollingPlayfield(ScrollingDirection direction, float? customWidth = null, float? customHeight = null)
: base(customWidth, customHeight) : base(customWidth, customHeight)
{ {
this.direction = direction; Direction.Value = direction;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -99,6 +99,11 @@ namespace osu.Game.Rulesets.UI.Scrolling
return false; return false;
} }
protected sealed override HitObjectContainer CreateHitObjectContainer() => new ScrollingHitObjectContainer(direction); protected sealed override HitObjectContainer CreateHitObjectContainer()
{
var container = new ScrollingHitObjectContainer();
container.Direction.BindTo(Direction);
return container;
}
} }
} }

View File

@ -93,6 +93,9 @@ namespace osu.Game.Rulesets.UI.Scrolling.Visualisers
/// <returns>A positive value indicating the position at <paramref name="time"/>.</returns> /// <returns>A positive value indicating the position at <paramref name="time"/>.</returns>
private double positionAt(double time, double timeRange) private double positionAt(double time, double timeRange)
{ {
if (controlPoints.Count == 0)
return time / timeRange;
double length = 0; double length = 0;
// We need to consider all timing points until the specified time and not just the currently-active one, // We need to consider all timing points until the specified time and not just the currently-active one,

View File

@ -185,9 +185,9 @@ namespace osu.Game.Screens.Edit
protected override bool OnScroll(InputState state) protected override bool OnScroll(InputState state)
{ {
if (state.Mouse.ScrollDelta.X + state.Mouse.ScrollDelta.Y > 0) if (state.Mouse.ScrollDelta.X + state.Mouse.ScrollDelta.Y > 0)
clock.SeekBackward(true); clock.SeekBackward(!clock.IsRunning);
else else
clock.SeekForward(true); clock.SeekForward(!clock.IsRunning);
return true; return true;
} }

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