mirror of
https://github.com/ppy/osu.git
synced 2025-03-05 10:53:10 +08:00
Merge branch 'master' into enable-ipc-test
This commit is contained in:
commit
f96effb2ea
18
.github/workflows/diffcalc.yml
vendored
18
.github/workflows/diffcalc.yml
vendored
@ -74,29 +74,35 @@ jobs:
|
|||||||
mkdir -p $GITHUB_WORKSPACE/master/
|
mkdir -p $GITHUB_WORKSPACE/master/
|
||||||
mkdir -p $GITHUB_WORKSPACE/pr/
|
mkdir -p $GITHUB_WORKSPACE/pr/
|
||||||
|
|
||||||
|
- name: Get upstream branch # https://akaimo.hatenablog.jp/entry/2020/05/16/101251
|
||||||
|
id: upstreambranch
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
echo "::set-output name=branchname::$(curl -H "Authorization: token ${GITHUB_TOKEN}" ${{ github.event.issue.pull_request.url }} | jq '.head.ref' | sed 's/\"//g')"
|
||||||
|
echo "::set-output name=repo::$(curl -H "Authorization: token ${GITHUB_TOKEN}" ${{ github.event.issue.pull_request.url }} | jq '.head.repo.full_name' | sed 's/\"//g')"
|
||||||
|
|
||||||
# Checkout osu
|
# Checkout osu
|
||||||
- name: Checkout osu (master)
|
- name: Checkout osu (master)
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
repository: peppy/osu
|
|
||||||
ref: 'diffcalc-optimisations'
|
|
||||||
path: 'master/osu'
|
path: 'master/osu'
|
||||||
- name: Checkout osu (pr)
|
- name: Checkout osu (pr)
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
path: 'pr/osu'
|
path: 'pr/osu'
|
||||||
|
repository: ${{ steps.upstreambranch.outputs.repo }}
|
||||||
|
ref: ${{ steps.upstreambranch.outputs.branchname }}
|
||||||
|
|
||||||
- name: Checkout osu-difficulty-calculator (master)
|
- name: Checkout osu-difficulty-calculator (master)
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
repository: peppy/osu-difficulty-calculator
|
repository: ppy/osu-difficulty-calculator
|
||||||
ref: 'bypass-attrib-row-insert'
|
|
||||||
path: 'master/osu-difficulty-calculator'
|
path: 'master/osu-difficulty-calculator'
|
||||||
- name: Checkout osu-difficulty-calculator (pr)
|
- name: Checkout osu-difficulty-calculator (pr)
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
repository: peppy/osu-difficulty-calculator
|
repository: ppy/osu-difficulty-calculator
|
||||||
ref: 'bypass-attrib-row-insert'
|
|
||||||
path: 'pr/osu-difficulty-calculator'
|
path: 'pr/osu-difficulty-calculator'
|
||||||
|
|
||||||
- name: Install .NET 5.0.x
|
- name: Install .NET 5.0.x
|
||||||
|
@ -52,7 +52,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.918.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.918.0" />
|
||||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.916.1" />
|
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.929.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Transitive Dependencies">
|
<ItemGroup Label="Transitive Dependencies">
|
||||||
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
|
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
|
||||||
|
@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
|||||||
|
|
||||||
// In 2B beatmaps, it is possible that a normal Fruit is placed in the middle of a JuiceStream.
|
// In 2B beatmaps, it is possible that a normal Fruit is placed in the middle of a JuiceStream.
|
||||||
foreach (var hitObject in beatmap.HitObjects
|
foreach (var hitObject in beatmap.HitObjects
|
||||||
.SelectMany(obj => obj is JuiceStream stream ? stream.NestedHitObjects : new[] { obj })
|
.SelectMany(obj => obj is JuiceStream stream ? stream.NestedHitObjects.AsEnumerable() : new[] { obj })
|
||||||
.Cast<CatchHitObject>()
|
.Cast<CatchHitObject>()
|
||||||
.OrderBy(x => x.StartTime))
|
.OrderBy(x => x.StartTime))
|
||||||
{
|
{
|
||||||
|
@ -149,7 +149,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
{
|
{
|
||||||
lowerBound ??= RandomStart;
|
lowerBound ??= RandomStart;
|
||||||
upperBound ??= TotalColumns;
|
upperBound ??= TotalColumns;
|
||||||
nextColumn ??= (_ => GetRandomColumn(lowerBound, upperBound));
|
nextColumn ??= _ => GetRandomColumn(lowerBound, upperBound);
|
||||||
|
|
||||||
// Check for the initial column
|
// Check for the initial column
|
||||||
if (isValid(initialColumn))
|
if (isValid(initialColumn))
|
||||||
@ -176,7 +176,19 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
|
|
||||||
return initialColumn;
|
return initialColumn;
|
||||||
|
|
||||||
bool isValid(int column) => validation?.Invoke(column) != false && !patterns.Any(p => p.ColumnHasObject(column));
|
bool isValid(int column)
|
||||||
|
{
|
||||||
|
if (validation?.Invoke(column) == false)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
foreach (var p in patterns)
|
||||||
|
{
|
||||||
|
if (p.ColumnHasObject(column))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -12,46 +12,68 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal class Pattern
|
internal class Pattern
|
||||||
{
|
{
|
||||||
private readonly List<ManiaHitObject> hitObjects = new List<ManiaHitObject>();
|
private List<ManiaHitObject> hitObjects;
|
||||||
|
private HashSet<int> containedColumns;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// All the hit objects contained in this pattern.
|
/// All the hit objects contained in this pattern.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IEnumerable<ManiaHitObject> HitObjects => hitObjects;
|
public IEnumerable<ManiaHitObject> HitObjects => hitObjects ?? Enumerable.Empty<ManiaHitObject>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Check whether a column of this patterns contains a hit object.
|
/// Check whether a column of this patterns contains a hit object.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="column">The column index.</param>
|
/// <param name="column">The column index.</param>
|
||||||
/// <returns>Whether the column with index <paramref name="column"/> contains a hit object.</returns>
|
/// <returns>Whether the column with index <paramref name="column"/> contains a hit object.</returns>
|
||||||
public bool ColumnHasObject(int column) => hitObjects.Exists(h => h.Column == column);
|
public bool ColumnHasObject(int column) => containedColumns?.Contains(column) == true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Amount of columns taken up by hit objects in this pattern.
|
/// Amount of columns taken up by hit objects in this pattern.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int ColumnWithObjects => HitObjects.GroupBy(h => h.Column).Count();
|
public int ColumnWithObjects => containedColumns?.Count ?? 0;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds a hit object to this pattern.
|
/// Adds a hit object to this pattern.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="hitObject">The hit object to add.</param>
|
/// <param name="hitObject">The hit object to add.</param>
|
||||||
public void Add(ManiaHitObject hitObject) => hitObjects.Add(hitObject);
|
public void Add(ManiaHitObject hitObject)
|
||||||
|
{
|
||||||
|
prepareStorage();
|
||||||
|
|
||||||
|
hitObjects.Add(hitObject);
|
||||||
|
containedColumns.Add(hitObject.Column);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Copies hit object from another pattern to this one.
|
/// Copies hit object from another pattern to this one.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="other">The other pattern.</param>
|
/// <param name="other">The other pattern.</param>
|
||||||
public void Add(Pattern other) => hitObjects.AddRange(other.HitObjects);
|
public void Add(Pattern other)
|
||||||
|
{
|
||||||
|
prepareStorage();
|
||||||
|
|
||||||
|
if (other.hitObjects != null)
|
||||||
|
{
|
||||||
|
hitObjects.AddRange(other.hitObjects);
|
||||||
|
|
||||||
|
foreach (var h in other.hitObjects)
|
||||||
|
containedColumns.Add(h.Column);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Clears this pattern, removing all hit objects.
|
/// Clears this pattern, removing all hit objects.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Clear() => hitObjects.Clear();
|
public void Clear()
|
||||||
|
{
|
||||||
|
hitObjects?.Clear();
|
||||||
|
containedColumns?.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
private void prepareStorage()
|
||||||
/// Removes a hit object from this pattern.
|
{
|
||||||
/// </summary>
|
hitObjects ??= new List<ManiaHitObject>();
|
||||||
/// <param name="hitObject">The hit object to remove.</param>
|
containedColumns ??= new HashSet<int>();
|
||||||
public bool Remove(ManiaHitObject hitObject) => hitObjects.Remove(hitObject);
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,78 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Rulesets.Osu.Edit;
|
||||||
|
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
|
||||||
|
using osu.Game.Tests.Visual;
|
||||||
|
using osuTK;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||||
|
{
|
||||||
|
public class TestSceneOsuEditorGrids : EditorTestScene
|
||||||
|
{
|
||||||
|
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestGridExclusivity()
|
||||||
|
{
|
||||||
|
AddStep("enable distance snap grid", () => InputManager.Key(Key.T));
|
||||||
|
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
|
||||||
|
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||||
|
rectangularGridActive(false);
|
||||||
|
|
||||||
|
AddStep("enable rectangular grid", () => InputManager.Key(Key.Y));
|
||||||
|
AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||||
|
rectangularGridActive(true);
|
||||||
|
|
||||||
|
AddStep("enable distance snap grid", () => InputManager.Key(Key.T));
|
||||||
|
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
|
||||||
|
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||||
|
rectangularGridActive(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void rectangularGridActive(bool active)
|
||||||
|
{
|
||||||
|
AddStep("choose placement tool", () => InputManager.Key(Key.Number2));
|
||||||
|
AddStep("move cursor to (1, 1)", () =>
|
||||||
|
{
|
||||||
|
var composer = Editor.ChildrenOfType<OsuRectangularPositionSnapGrid>().Single();
|
||||||
|
InputManager.MoveMouseTo(composer.ToScreenSpace(new Vector2(1, 1)));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (active)
|
||||||
|
AddAssert("placement blueprint at (0, 0)", () => Precision.AlmostEquals(Editor.ChildrenOfType<HitCirclePlacementBlueprint>().Single().HitObject.Position, new Vector2(0, 0)));
|
||||||
|
else
|
||||||
|
AddAssert("placement blueprint at (1, 1)", () => Precision.AlmostEquals(Editor.ChildrenOfType<HitCirclePlacementBlueprint>().Single().HitObject.Position, new Vector2(1, 1)));
|
||||||
|
|
||||||
|
AddStep("choose selection tool", () => InputManager.Key(Key.Number1));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestGridSizeToggling()
|
||||||
|
{
|
||||||
|
AddStep("enable rectangular grid", () => InputManager.Key(Key.Y));
|
||||||
|
AddUntilStep("rectangular grid visible", () => this.ChildrenOfType<OsuRectangularPositionSnapGrid>().Any());
|
||||||
|
gridSizeIs(4);
|
||||||
|
|
||||||
|
nextGridSizeIs(8);
|
||||||
|
nextGridSizeIs(16);
|
||||||
|
nextGridSizeIs(32);
|
||||||
|
nextGridSizeIs(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void nextGridSizeIs(int size)
|
||||||
|
{
|
||||||
|
AddStep("toggle to next grid size", () => InputManager.Key(Key.G));
|
||||||
|
gridSizeIs(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void gridSizeIs(int size)
|
||||||
|
=> AddAssert($"grid size is {size}", () => this.ChildrenOfType<OsuRectangularPositionSnapGrid>().Single().Spacing == new Vector2(size)
|
||||||
|
&& EditorBeatmap.BeatmapInfo.GridSize == size);
|
||||||
|
}
|
||||||
|
}
|
@ -21,6 +21,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
public class OsuDifficultyCalculator : DifficultyCalculator
|
public class OsuDifficultyCalculator : DifficultyCalculator
|
||||||
{
|
{
|
||||||
private const double difficulty_multiplier = 0.0675;
|
private const double difficulty_multiplier = 0.0675;
|
||||||
|
private double hitWindowGreat;
|
||||||
|
|
||||||
public OsuDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
|
public OsuDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
|
||||||
: base(ruleset, beatmap)
|
: base(ruleset, beatmap)
|
||||||
@ -52,11 +53,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
|
|
||||||
double starRating = basePerformance > 0.00001 ? Math.Cbrt(1.12) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0;
|
double starRating = basePerformance > 0.00001 ? Math.Cbrt(1.12) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0;
|
||||||
|
|
||||||
HitWindows hitWindows = new OsuHitWindows();
|
|
||||||
hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
|
|
||||||
|
|
||||||
// Todo: These int casts are temporary to achieve 1:1 results with osu!stable, and should be removed in the future
|
|
||||||
double hitWindowGreat = (int)(hitWindows.WindowFor(HitResult.Great)) / clockRate;
|
|
||||||
double preempt = (int)BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
double preempt = (int)BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||||
|
|
||||||
int maxCombo = beatmap.HitObjects.Count;
|
int maxCombo = beatmap.HitObjects.Count;
|
||||||
@ -96,12 +92,21 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[]
|
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
|
||||||
|
{
|
||||||
|
HitWindows hitWindows = new OsuHitWindows();
|
||||||
|
hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
|
||||||
|
|
||||||
|
// Todo: These int casts are temporary to achieve 1:1 results with osu!stable, and should be removed in the future
|
||||||
|
hitWindowGreat = (int)(hitWindows.WindowFor(HitResult.Great)) / clockRate;
|
||||||
|
|
||||||
|
return new Skill[]
|
||||||
{
|
{
|
||||||
new Aim(mods),
|
new Aim(mods),
|
||||||
new Speed(mods),
|
new Speed(mods, hitWindowGreat),
|
||||||
new Flashlight(mods)
|
new Flashlight(mods)
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
protected override Mod[] DifficultyAdjustmentMods => new Mod[]
|
protected override Mod[] DifficultyAdjustmentMods => new Mod[]
|
||||||
{
|
{
|
||||||
|
@ -16,6 +16,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
|||||||
|
|
||||||
protected new OsuHitObject BaseObject => (OsuHitObject)base.BaseObject;
|
protected new OsuHitObject BaseObject => (OsuHitObject)base.BaseObject;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Milliseconds elapsed since the start time of the previous <see cref="OsuDifficultyHitObject"/>, with a minimum of 25ms to account for simultaneous <see cref="OsuDifficultyHitObject"/>s.
|
||||||
|
/// </summary>
|
||||||
|
public double StrainTime { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Normalized distance from the end position of the previous <see cref="OsuDifficultyHitObject"/> to the start position of this <see cref="OsuDifficultyHitObject"/>.
|
/// Normalized distance from the end position of the previous <see cref="OsuDifficultyHitObject"/> to the start position of this <see cref="OsuDifficultyHitObject"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -32,11 +37,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public double? Angle { get; private set; }
|
public double? Angle { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Milliseconds elapsed since the start time of the previous <see cref="OsuDifficultyHitObject"/>, with a minimum of 50ms.
|
|
||||||
/// </summary>
|
|
||||||
public readonly double StrainTime;
|
|
||||||
|
|
||||||
private readonly OsuHitObject lastLastObject;
|
private readonly OsuHitObject lastLastObject;
|
||||||
private readonly OsuHitObject lastObject;
|
private readonly OsuHitObject lastObject;
|
||||||
|
|
||||||
@ -48,8 +48,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
|||||||
|
|
||||||
setDistances();
|
setDistances();
|
||||||
|
|
||||||
// Every strain interval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure
|
// Capped to 25ms to prevent difficulty calculation breaking from simulatenous objects.
|
||||||
StrainTime = Math.Max(50, DeltaTime);
|
StrainTime = Math.Max(DeltaTime, 25);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setDistances()
|
private void setDistances()
|
||||||
|
@ -6,6 +6,7 @@ using osu.Game.Rulesets.Difficulty.Preprocessing;
|
|||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Framework.Utils;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||||
{
|
{
|
||||||
@ -26,12 +27,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
protected override double DifficultyMultiplier => 1.04;
|
protected override double DifficultyMultiplier => 1.04;
|
||||||
|
|
||||||
private const double min_speed_bonus = 75; // ~200BPM
|
private const double min_speed_bonus = 75; // ~200BPM
|
||||||
private const double max_speed_bonus = 45; // ~330BPM
|
|
||||||
private const double speed_balancing_factor = 40;
|
private const double speed_balancing_factor = 40;
|
||||||
|
|
||||||
public Speed(Mod[] mods)
|
private readonly double greatWindow;
|
||||||
|
|
||||||
|
public Speed(Mod[] mods, double hitWindowGreat)
|
||||||
: base(mods)
|
: base(mods)
|
||||||
{
|
{
|
||||||
|
greatWindow = hitWindowGreat;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override double StrainValueOf(DifficultyHitObject current)
|
protected override double StrainValueOf(DifficultyHitObject current)
|
||||||
@ -40,13 +43,25 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
var osuCurrent = (OsuDifficultyHitObject)current;
|
var osuCurrent = (OsuDifficultyHitObject)current;
|
||||||
|
var osuPrevious = Previous.Count > 0 ? (OsuDifficultyHitObject)Previous[0] : null;
|
||||||
|
|
||||||
double distance = Math.Min(single_spacing_threshold, osuCurrent.TravelDistance + osuCurrent.JumpDistance);
|
double distance = Math.Min(single_spacing_threshold, osuCurrent.TravelDistance + osuCurrent.JumpDistance);
|
||||||
double deltaTime = Math.Max(max_speed_bonus, current.DeltaTime);
|
double strainTime = osuCurrent.StrainTime;
|
||||||
|
|
||||||
|
double greatWindowFull = greatWindow * 2;
|
||||||
|
double speedWindowRatio = strainTime / greatWindowFull;
|
||||||
|
|
||||||
|
// Aim to nerf cheesy rhythms (Very fast consecutive doubles with large deltatimes between)
|
||||||
|
if (osuPrevious != null && strainTime < greatWindowFull && osuPrevious.StrainTime > strainTime)
|
||||||
|
strainTime = Interpolation.Lerp(osuPrevious.StrainTime, strainTime, speedWindowRatio);
|
||||||
|
|
||||||
|
// Cap deltatime to the OD 300 hitwindow.
|
||||||
|
// 0.93 is derived from making sure 260bpm OD8 streams aren't nerfed harshly, whilst 0.92 limits the effect of the cap.
|
||||||
|
strainTime /= Math.Clamp((strainTime / greatWindowFull) / 0.93, 0.92, 1);
|
||||||
|
|
||||||
double speedBonus = 1.0;
|
double speedBonus = 1.0;
|
||||||
if (deltaTime < min_speed_bonus)
|
if (strainTime < min_speed_bonus)
|
||||||
speedBonus = 1 + Math.Pow((min_speed_bonus - deltaTime) / speed_balancing_factor, 2);
|
speedBonus = 1 + Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2);
|
||||||
|
|
||||||
double angleBonus = 1.0;
|
double angleBonus = 1.0;
|
||||||
|
|
||||||
@ -64,7 +79,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (1 + (speedBonus - 1) * 0.75) * angleBonus * (0.95 + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) / osuCurrent.StrainTime;
|
return (1 + (speedBonus - 1) * 0.75)
|
||||||
|
* angleBonus
|
||||||
|
* (0.95 + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5))
|
||||||
|
/ strainTime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,10 +42,12 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
};
|
};
|
||||||
|
|
||||||
private readonly Bindable<TernaryState> distanceSnapToggle = new Bindable<TernaryState>();
|
private readonly Bindable<TernaryState> distanceSnapToggle = new Bindable<TernaryState>();
|
||||||
|
private readonly Bindable<TernaryState> rectangularGridSnapToggle = new Bindable<TernaryState>();
|
||||||
|
|
||||||
protected override IEnumerable<TernaryButton> CreateTernaryButtons() => base.CreateTernaryButtons().Concat(new[]
|
protected override IEnumerable<TernaryButton> CreateTernaryButtons() => base.CreateTernaryButtons().Concat(new[]
|
||||||
{
|
{
|
||||||
new TernaryButton(distanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler })
|
new TernaryButton(distanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler }),
|
||||||
|
new TernaryButton(rectangularGridSnapToggle, "Grid Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Th })
|
||||||
});
|
});
|
||||||
|
|
||||||
private BindableList<HitObject> selectedHitObjects;
|
private BindableList<HitObject> selectedHitObjects;
|
||||||
@ -63,6 +65,10 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
PlayfieldBorderStyle = { Value = PlayfieldBorderStyle.Corners }
|
PlayfieldBorderStyle = { Value = PlayfieldBorderStyle.Corners }
|
||||||
},
|
},
|
||||||
distanceSnapGridContainer = new Container
|
distanceSnapGridContainer = new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both
|
||||||
|
},
|
||||||
|
rectangularPositionSnapGrid = new OsuRectangularPositionSnapGrid
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both
|
RelativeSizeAxes = Axes.Both
|
||||||
}
|
}
|
||||||
@ -73,7 +79,19 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
|
|
||||||
placementObject = EditorBeatmap.PlacementObject.GetBoundCopy();
|
placementObject = EditorBeatmap.PlacementObject.GetBoundCopy();
|
||||||
placementObject.ValueChanged += _ => updateDistanceSnapGrid();
|
placementObject.ValueChanged += _ => updateDistanceSnapGrid();
|
||||||
distanceSnapToggle.ValueChanged += _ => updateDistanceSnapGrid();
|
distanceSnapToggle.ValueChanged += _ =>
|
||||||
|
{
|
||||||
|
updateDistanceSnapGrid();
|
||||||
|
|
||||||
|
if (distanceSnapToggle.Value == TernaryState.True)
|
||||||
|
rectangularGridSnapToggle.Value = TernaryState.False;
|
||||||
|
};
|
||||||
|
|
||||||
|
rectangularGridSnapToggle.ValueChanged += _ =>
|
||||||
|
{
|
||||||
|
if (rectangularGridSnapToggle.Value == TernaryState.True)
|
||||||
|
distanceSnapToggle.Value = TernaryState.False;
|
||||||
|
};
|
||||||
|
|
||||||
// we may be entering the screen with a selection already active
|
// we may be entering the screen with a selection already active
|
||||||
updateDistanceSnapGrid();
|
updateDistanceSnapGrid();
|
||||||
@ -91,6 +109,8 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
private readonly Cached distanceSnapGridCache = new Cached();
|
private readonly Cached distanceSnapGridCache = new Cached();
|
||||||
private double? lastDistanceSnapGridTime;
|
private double? lastDistanceSnapGridTime;
|
||||||
|
|
||||||
|
private RectangularPositionSnapGrid rectangularPositionSnapGrid;
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
{
|
{
|
||||||
base.Update();
|
base.Update();
|
||||||
@ -122,15 +142,21 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
if (positionSnap.ScreenSpacePosition != screenSpacePosition)
|
if (positionSnap.ScreenSpacePosition != screenSpacePosition)
|
||||||
return positionSnap;
|
return positionSnap;
|
||||||
|
|
||||||
// will be null if distance snap is disabled or not feasible for the current time value.
|
if (distanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
|
||||||
if (distanceSnapGrid == null)
|
{
|
||||||
return base.SnapScreenSpacePositionToValidTime(screenSpacePosition);
|
|
||||||
|
|
||||||
(Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition));
|
(Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition));
|
||||||
|
|
||||||
return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, PlayfieldAtScreenSpacePosition(screenSpacePosition));
|
return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, PlayfieldAtScreenSpacePosition(screenSpacePosition));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (rectangularGridSnapToggle.Value == TernaryState.True)
|
||||||
|
{
|
||||||
|
Vector2 pos = rectangularPositionSnapGrid.GetSnappedPosition(rectangularPositionSnapGrid.ToLocalSpace(screenSpacePosition));
|
||||||
|
return new SnapResult(rectangularPositionSnapGrid.ToScreenSpace(pos), null, PlayfieldAtScreenSpacePosition(screenSpacePosition));
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.SnapScreenSpacePositionToValidTime(screenSpacePosition);
|
||||||
|
}
|
||||||
|
|
||||||
private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult)
|
private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult)
|
||||||
{
|
{
|
||||||
// check other on-screen objects for snapping/stacking
|
// check other on-screen objects for snapping/stacking
|
||||||
|
69
osu.Game.Rulesets.Osu/Edit/OsuRectangularPositionSnapGrid.cs
Normal file
69
osu.Game.Rulesets.Osu/Edit/OsuRectangularPositionSnapGrid.cs
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Input.Bindings;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Game.Input.Bindings;
|
||||||
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
|
using osu.Game.Screens.Edit;
|
||||||
|
using osu.Game.Screens.Edit.Compose.Components;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Edit
|
||||||
|
{
|
||||||
|
public class OsuRectangularPositionSnapGrid : RectangularPositionSnapGrid, IKeyBindingHandler<GlobalAction>
|
||||||
|
{
|
||||||
|
private static readonly int[] grid_sizes = { 4, 8, 16, 32 };
|
||||||
|
|
||||||
|
private int currentGridSizeIndex = grid_sizes.Length - 1;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private EditorBeatmap editorBeatmap { get; set; }
|
||||||
|
|
||||||
|
public OsuRectangularPositionSnapGrid()
|
||||||
|
: base(OsuPlayfield.BASE_SIZE / 2)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
var gridSizeIndex = Array.IndexOf(grid_sizes, editorBeatmap.BeatmapInfo.GridSize);
|
||||||
|
if (gridSizeIndex >= 0)
|
||||||
|
currentGridSizeIndex = gridSizeIndex;
|
||||||
|
updateSpacing();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void nextGridSize()
|
||||||
|
{
|
||||||
|
currentGridSizeIndex = (currentGridSizeIndex + 1) % grid_sizes.Length;
|
||||||
|
updateSpacing();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateSpacing()
|
||||||
|
{
|
||||||
|
int gridSize = grid_sizes[currentGridSizeIndex];
|
||||||
|
|
||||||
|
editorBeatmap.BeatmapInfo.GridSize = gridSize;
|
||||||
|
Spacing = new Vector2(gridSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||||
|
{
|
||||||
|
switch (e.Action)
|
||||||
|
{
|
||||||
|
case GlobalAction.EditorCycleGridDisplayMode:
|
||||||
|
nextGridSize();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -13,26 +13,20 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
{
|
{
|
||||||
public class LegacyReverseArrow : CompositeDrawable
|
public class LegacyReverseArrow : CompositeDrawable
|
||||||
{
|
{
|
||||||
private ISkin skin { get; }
|
|
||||||
|
|
||||||
[Resolved(canBeNull: true)]
|
[Resolved(canBeNull: true)]
|
||||||
private DrawableHitObject drawableHitObject { get; set; }
|
private DrawableHitObject drawableHitObject { get; set; }
|
||||||
|
|
||||||
private Drawable proxy;
|
private Drawable proxy;
|
||||||
|
|
||||||
public LegacyReverseArrow(ISkin skin)
|
|
||||||
{
|
|
||||||
this.skin = skin;
|
|
||||||
}
|
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load(ISkinSource skinSource)
|
||||||
{
|
{
|
||||||
AutoSizeAxes = Axes.Both;
|
AutoSizeAxes = Axes.Both;
|
||||||
|
|
||||||
string lookupName = new OsuSkinComponent(OsuSkinComponents.ReverseArrow).LookupName;
|
string lookupName = new OsuSkinComponent(OsuSkinComponents.ReverseArrow).LookupName;
|
||||||
|
|
||||||
InternalChild = skin.GetAnimation(lookupName, true, true) ?? Empty();
|
var skin = skinSource.FindProvider(s => s.GetTexture(lookupName) != null);
|
||||||
|
InternalChild = skin?.GetAnimation(lookupName, true, true) ?? Empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
|
@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
|
|
||||||
case OsuSkinComponents.ReverseArrow:
|
case OsuSkinComponents.ReverseArrow:
|
||||||
if (hasHitCircle.Value)
|
if (hasHitCircle.Value)
|
||||||
return new LegacyReverseArrow(this);
|
return new LegacyReverseArrow();
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
|
132
osu.Game.Tests/Visual/Editing/TestSceneBlueprintOrdering.cs
Normal file
132
osu.Game.Tests/Visual/Editing/TestSceneBlueprintOrdering.cs
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
|
using osu.Game.Rulesets.Osu;
|
||||||
|
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
|
||||||
|
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
|
using osu.Game.Rulesets.UI;
|
||||||
|
using osu.Game.Screens.Edit.Compose.Components;
|
||||||
|
using osu.Game.Screens.Edit.Compose.Components.Timeline;
|
||||||
|
using osu.Game.Tests.Beatmaps;
|
||||||
|
using osuTK;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.Editing
|
||||||
|
{
|
||||||
|
public class TestSceneBlueprintOrdering : EditorTestScene
|
||||||
|
{
|
||||||
|
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
|
||||||
|
|
||||||
|
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
|
||||||
|
|
||||||
|
private EditorBlueprintContainer blueprintContainer
|
||||||
|
=> Editor.ChildrenOfType<EditorBlueprintContainer>().First();
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSelectedObjectHasPriorityWhenOverlapping()
|
||||||
|
{
|
||||||
|
var firstSlider = new Slider
|
||||||
|
{
|
||||||
|
Path = new SliderPath(new[]
|
||||||
|
{
|
||||||
|
new PathControlPoint(new Vector2()),
|
||||||
|
new PathControlPoint(new Vector2(150, -50)),
|
||||||
|
new PathControlPoint(new Vector2(300, 0))
|
||||||
|
}),
|
||||||
|
Position = new Vector2(0, 100)
|
||||||
|
};
|
||||||
|
var secondSlider = new Slider
|
||||||
|
{
|
||||||
|
Path = new SliderPath(new[]
|
||||||
|
{
|
||||||
|
new PathControlPoint(new Vector2()),
|
||||||
|
new PathControlPoint(new Vector2(-50, 50)),
|
||||||
|
new PathControlPoint(new Vector2(-100, 100))
|
||||||
|
}),
|
||||||
|
Position = new Vector2(200, 0)
|
||||||
|
};
|
||||||
|
|
||||||
|
AddStep("add overlapping sliders", () =>
|
||||||
|
{
|
||||||
|
EditorBeatmap.Add(firstSlider);
|
||||||
|
EditorBeatmap.Add(secondSlider);
|
||||||
|
});
|
||||||
|
AddStep("select first slider", () => EditorBeatmap.SelectedHitObjects.Add(firstSlider));
|
||||||
|
|
||||||
|
AddStep("move mouse to common point", () =>
|
||||||
|
{
|
||||||
|
var pos = blueprintContainer.ChildrenOfType<PathControlPointPiece>().ElementAt(1).ScreenSpaceDrawQuad.Centre;
|
||||||
|
InputManager.MoveMouseTo(pos);
|
||||||
|
});
|
||||||
|
AddStep("right click", () => InputManager.Click(MouseButton.Right));
|
||||||
|
|
||||||
|
AddAssert("selection is unchanged", () => EditorBeatmap.SelectedHitObjects.Single() == firstSlider);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestOverlappingObjectsWithSameStartTime()
|
||||||
|
{
|
||||||
|
AddStep("add overlapping circles", () =>
|
||||||
|
{
|
||||||
|
EditorBeatmap.Add(createHitCircle(50, OsuPlayfield.BASE_SIZE / 2));
|
||||||
|
EditorBeatmap.Add(createHitCircle(50, OsuPlayfield.BASE_SIZE / 2 + new Vector2(-10, -20)));
|
||||||
|
EditorBeatmap.Add(createHitCircle(50, OsuPlayfield.BASE_SIZE / 2 + new Vector2(10, -20)));
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("click at centre of playfield", () =>
|
||||||
|
{
|
||||||
|
var hitObjectContainer = Editor.ChildrenOfType<HitObjectContainer>().Single();
|
||||||
|
var centre = hitObjectContainer.ToScreenSpace(OsuPlayfield.BASE_SIZE / 2);
|
||||||
|
InputManager.MoveMouseTo(centre);
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAssert("frontmost object selected", () =>
|
||||||
|
{
|
||||||
|
var hasCombo = Editor.ChildrenOfType<HitCircleSelectionBlueprint>().Single(b => b.IsSelected).Item as IHasComboInformation;
|
||||||
|
return hasCombo?.IndexInCurrentCombo == 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPlacementOfConcurrentObjectWithDuration()
|
||||||
|
{
|
||||||
|
AddStep("seek to timing point", () => EditorClock.Seek(2170));
|
||||||
|
AddStep("add hit circle", () => EditorBeatmap.Add(createHitCircle(2170, Vector2.Zero)));
|
||||||
|
|
||||||
|
AddStep("choose spinner placement tool", () =>
|
||||||
|
{
|
||||||
|
InputManager.Key(Key.Number4);
|
||||||
|
var hitObjectContainer = Editor.ChildrenOfType<HitObjectContainer>().Single();
|
||||||
|
InputManager.MoveMouseTo(hitObjectContainer.ScreenSpaceDrawQuad.Centre);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("begin placing spinner", () =>
|
||||||
|
{
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
AddStep("end placing spinner", () =>
|
||||||
|
{
|
||||||
|
EditorClock.Seek(2500);
|
||||||
|
InputManager.Click(MouseButton.Right);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAssert("two timeline blueprints present", () => Editor.ChildrenOfType<TimelineHitObjectBlueprint>().Count() == 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private HitCircle createHitCircle(double startTime, Vector2 position) => new HitCircle
|
||||||
|
{
|
||||||
|
StartTime = startTime,
|
||||||
|
Position = position,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -1,70 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
using System.Linq;
|
|
||||||
using NUnit.Framework;
|
|
||||||
using osu.Framework.Testing;
|
|
||||||
using osu.Game.Beatmaps;
|
|
||||||
using osu.Game.Rulesets;
|
|
||||||
using osu.Game.Rulesets.Objects;
|
|
||||||
using osu.Game.Rulesets.Osu;
|
|
||||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
|
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
|
||||||
using osu.Game.Screens.Edit.Compose.Components;
|
|
||||||
using osu.Game.Tests.Beatmaps;
|
|
||||||
using osuTK;
|
|
||||||
using osuTK.Input;
|
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Editing
|
|
||||||
{
|
|
||||||
public class TestSceneBlueprintSelection : EditorTestScene
|
|
||||||
{
|
|
||||||
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
|
|
||||||
|
|
||||||
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
|
|
||||||
|
|
||||||
private EditorBlueprintContainer blueprintContainer
|
|
||||||
=> Editor.ChildrenOfType<EditorBlueprintContainer>().First();
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestSelectedObjectHasPriorityWhenOverlapping()
|
|
||||||
{
|
|
||||||
var firstSlider = new Slider
|
|
||||||
{
|
|
||||||
Path = new SliderPath(new[]
|
|
||||||
{
|
|
||||||
new PathControlPoint(new Vector2()),
|
|
||||||
new PathControlPoint(new Vector2(150, -50)),
|
|
||||||
new PathControlPoint(new Vector2(300, 0))
|
|
||||||
}),
|
|
||||||
Position = new Vector2(0, 100)
|
|
||||||
};
|
|
||||||
var secondSlider = new Slider
|
|
||||||
{
|
|
||||||
Path = new SliderPath(new[]
|
|
||||||
{
|
|
||||||
new PathControlPoint(new Vector2()),
|
|
||||||
new PathControlPoint(new Vector2(-50, 50)),
|
|
||||||
new PathControlPoint(new Vector2(-100, 100))
|
|
||||||
}),
|
|
||||||
Position = new Vector2(200, 0)
|
|
||||||
};
|
|
||||||
|
|
||||||
AddStep("add overlapping sliders", () =>
|
|
||||||
{
|
|
||||||
EditorBeatmap.Add(firstSlider);
|
|
||||||
EditorBeatmap.Add(secondSlider);
|
|
||||||
});
|
|
||||||
AddStep("select first slider", () => EditorBeatmap.SelectedHitObjects.Add(firstSlider));
|
|
||||||
|
|
||||||
AddStep("move mouse to common point", () =>
|
|
||||||
{
|
|
||||||
var pos = blueprintContainer.ChildrenOfType<PathControlPointPiece>().ElementAt(1).ScreenSpaceDrawQuad.Centre;
|
|
||||||
InputManager.MoveMouseTo(pos);
|
|
||||||
});
|
|
||||||
AddStep("right click", () => InputManager.Click(MouseButton.Right));
|
|
||||||
|
|
||||||
AddAssert("selection is unchanged", () => EditorBeatmap.SelectedHitObjects.Single() == firstSlider);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -20,14 +20,14 @@ using osuTK.Input;
|
|||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Editing
|
namespace osu.Game.Tests.Visual.Editing
|
||||||
{
|
{
|
||||||
public class TestSceneEditorSelection : EditorTestScene
|
public class TestSceneComposerSelection : EditorTestScene
|
||||||
{
|
{
|
||||||
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
|
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
|
||||||
|
|
||||||
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
|
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
|
||||||
|
|
||||||
private EditorBlueprintContainer blueprintContainer
|
private ComposeBlueprintContainer blueprintContainer
|
||||||
=> Editor.ChildrenOfType<EditorBlueprintContainer>().First();
|
=> Editor.ChildrenOfType<ComposeBlueprintContainer>().First();
|
||||||
|
|
||||||
private void moveMouseToObject(Func<HitObject> targetFunc)
|
private void moveMouseToObject(Func<HitObject> targetFunc)
|
||||||
{
|
{
|
@ -0,0 +1,104 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Game.Screens.Edit.Compose.Components;
|
||||||
|
using osuTK;
|
||||||
|
using osuTK.Graphics;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.Editing
|
||||||
|
{
|
||||||
|
public class TestSceneRectangularPositionSnapGrid : OsuManualInputManagerTestScene
|
||||||
|
{
|
||||||
|
private Container content;
|
||||||
|
protected override Container<Drawable> Content => content;
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
base.Content.AddRange(new Drawable[]
|
||||||
|
{
|
||||||
|
new Box
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Colour = Colour4.Gray
|
||||||
|
},
|
||||||
|
content = new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly object[][] test_cases =
|
||||||
|
{
|
||||||
|
new object[] { new Vector2(0, 0), new Vector2(10, 10) },
|
||||||
|
new object[] { new Vector2(240, 180), new Vector2(10, 15) },
|
||||||
|
new object[] { new Vector2(160, 120), new Vector2(30, 20) },
|
||||||
|
new object[] { new Vector2(480, 360), new Vector2(100, 100) },
|
||||||
|
};
|
||||||
|
|
||||||
|
[TestCaseSource(nameof(test_cases))]
|
||||||
|
public void TestRectangularGrid(Vector2 position, Vector2 spacing)
|
||||||
|
{
|
||||||
|
RectangularPositionSnapGrid grid = null;
|
||||||
|
|
||||||
|
AddStep("create grid", () => Child = grid = new RectangularPositionSnapGrid(position)
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Spacing = spacing
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("add snapping cursor", () => Add(new SnappingCursorContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
GetSnapPosition = pos => grid.GetSnappedPosition(grid.ToLocalSpace(pos))
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SnappingCursorContainer : CompositeDrawable
|
||||||
|
{
|
||||||
|
public Func<Vector2, Vector2> GetSnapPosition;
|
||||||
|
|
||||||
|
private readonly Drawable cursor;
|
||||||
|
|
||||||
|
public SnappingCursorContainer()
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
|
||||||
|
InternalChild = cursor = new Circle
|
||||||
|
{
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Size = new Vector2(50),
|
||||||
|
Colour = Color4.Red
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
updatePosition(GetContainingInputManager().CurrentState.Mouse.Position);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnMouseMove(MouseMoveEvent e)
|
||||||
|
{
|
||||||
|
base.OnMouseMove(e);
|
||||||
|
|
||||||
|
updatePosition(e.ScreenSpaceMousePosition);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updatePosition(Vector2 screenSpacePosition)
|
||||||
|
{
|
||||||
|
cursor.Position = GetSnapPosition.Invoke(screenSpacePosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
266
osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs
Normal file
266
osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Osu;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Screens.Edit.Compose.Components.Timeline;
|
||||||
|
using osu.Game.Tests.Beatmaps;
|
||||||
|
using osuTK;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.Editing
|
||||||
|
{
|
||||||
|
public class TestSceneTimelineSelection : EditorTestScene
|
||||||
|
{
|
||||||
|
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
|
||||||
|
|
||||||
|
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
|
||||||
|
|
||||||
|
private TimelineBlueprintContainer blueprintContainer
|
||||||
|
=> Editor.ChildrenOfType<TimelineBlueprintContainer>().First();
|
||||||
|
|
||||||
|
private void moveMouseToObject(Func<HitObject> targetFunc)
|
||||||
|
{
|
||||||
|
AddStep("move mouse to object", () =>
|
||||||
|
{
|
||||||
|
var pos = blueprintContainer.SelectionBlueprints
|
||||||
|
.First(s => s.Item == targetFunc())
|
||||||
|
.ChildrenOfType<TimelineHitObjectBlueprint>()
|
||||||
|
.First().ScreenSpaceDrawQuad.Centre;
|
||||||
|
|
||||||
|
InputManager.MoveMouseTo(pos);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestNudgeSelection()
|
||||||
|
{
|
||||||
|
HitCircle[] addedObjects = null;
|
||||||
|
|
||||||
|
AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects = new[]
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 100 },
|
||||||
|
new HitCircle { StartTime = 200, Position = new Vector2(100) },
|
||||||
|
new HitCircle { StartTime = 300, Position = new Vector2(200) },
|
||||||
|
new HitCircle { StartTime = 400, Position = new Vector2(300) },
|
||||||
|
}));
|
||||||
|
|
||||||
|
AddStep("select objects", () => EditorBeatmap.SelectedHitObjects.AddRange(addedObjects));
|
||||||
|
|
||||||
|
AddStep("nudge forwards", () => InputManager.Key(Key.K));
|
||||||
|
AddAssert("objects moved forwards in time", () => addedObjects[0].StartTime > 100);
|
||||||
|
|
||||||
|
AddStep("nudge backwards", () => InputManager.Key(Key.J));
|
||||||
|
AddAssert("objects reverted to original position", () => addedObjects[0].StartTime == 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestBasicSelect()
|
||||||
|
{
|
||||||
|
var addedObject = new HitCircle { StartTime = 100 };
|
||||||
|
AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));
|
||||||
|
|
||||||
|
moveMouseToObject(() => addedObject);
|
||||||
|
AddStep("left click", () => InputManager.Click(MouseButton.Left));
|
||||||
|
|
||||||
|
AddAssert("hitobject selected", () => EditorBeatmap.SelectedHitObjects.Single() == addedObject);
|
||||||
|
|
||||||
|
var addedObject2 = new HitCircle
|
||||||
|
{
|
||||||
|
StartTime = 200,
|
||||||
|
Position = new Vector2(100),
|
||||||
|
};
|
||||||
|
|
||||||
|
AddStep("add one more hitobject", () => EditorBeatmap.Add(addedObject2));
|
||||||
|
AddAssert("selection unchanged", () => EditorBeatmap.SelectedHitObjects.Single() == addedObject);
|
||||||
|
|
||||||
|
moveMouseToObject(() => addedObject2);
|
||||||
|
AddStep("left click", () => InputManager.Click(MouseButton.Left));
|
||||||
|
AddAssert("hitobject selected", () => EditorBeatmap.SelectedHitObjects.Single() == addedObject2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMultiSelect()
|
||||||
|
{
|
||||||
|
var addedObjects = new[]
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 100 },
|
||||||
|
new HitCircle { StartTime = 200, Position = new Vector2(100) },
|
||||||
|
new HitCircle { StartTime = 300, Position = new Vector2(200) },
|
||||||
|
new HitCircle { StartTime = 400, Position = new Vector2(300) },
|
||||||
|
};
|
||||||
|
|
||||||
|
AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects));
|
||||||
|
|
||||||
|
moveMouseToObject(() => addedObjects[0]);
|
||||||
|
AddStep("click first", () => InputManager.Click(MouseButton.Left));
|
||||||
|
|
||||||
|
AddAssert("hitobject selected", () => EditorBeatmap.SelectedHitObjects.Single() == addedObjects[0]);
|
||||||
|
|
||||||
|
AddStep("hold control", () => InputManager.PressKey(Key.ControlLeft));
|
||||||
|
|
||||||
|
moveMouseToObject(() => addedObjects[1]);
|
||||||
|
AddStep("click second", () => InputManager.Click(MouseButton.Left));
|
||||||
|
AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 2 && EditorBeatmap.SelectedHitObjects.Contains(addedObjects[1]));
|
||||||
|
|
||||||
|
moveMouseToObject(() => addedObjects[2]);
|
||||||
|
AddStep("click third", () => InputManager.Click(MouseButton.Left));
|
||||||
|
AddAssert("3 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 3 && EditorBeatmap.SelectedHitObjects.Contains(addedObjects[2]));
|
||||||
|
|
||||||
|
moveMouseToObject(() => addedObjects[1]);
|
||||||
|
AddStep("click second", () => InputManager.Click(MouseButton.Left));
|
||||||
|
AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 2 && !EditorBeatmap.SelectedHitObjects.Contains(addedObjects[1]));
|
||||||
|
|
||||||
|
AddStep("release control", () => InputManager.ReleaseKey(Key.ControlLeft));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestBasicDeselect()
|
||||||
|
{
|
||||||
|
var addedObject = new HitCircle { StartTime = 100 };
|
||||||
|
AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));
|
||||||
|
|
||||||
|
moveMouseToObject(() => addedObject);
|
||||||
|
AddStep("left click", () => InputManager.Click(MouseButton.Left));
|
||||||
|
|
||||||
|
AddAssert("hitobject selected", () => EditorBeatmap.SelectedHitObjects.Single() == addedObject);
|
||||||
|
|
||||||
|
AddStep("click away", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(Editor.ChildrenOfType<TimelineArea>().Single().ScreenSpaceDrawQuad.TopLeft + Vector2.One);
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAssert("selection lost", () => EditorBeatmap.SelectedHitObjects.Count == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestQuickDelete()
|
||||||
|
{
|
||||||
|
var addedObject = new HitCircle
|
||||||
|
{
|
||||||
|
StartTime = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));
|
||||||
|
|
||||||
|
moveMouseToObject(() => addedObject);
|
||||||
|
|
||||||
|
AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft));
|
||||||
|
AddStep("right click", () => InputManager.Click(MouseButton.Right));
|
||||||
|
AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
|
||||||
|
|
||||||
|
AddAssert("no hitobjects in beatmap", () => EditorBeatmap.HitObjects.Count == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestRangeSelect()
|
||||||
|
{
|
||||||
|
var addedObjects = new[]
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 100 },
|
||||||
|
new HitCircle { StartTime = 200, Position = new Vector2(100) },
|
||||||
|
new HitCircle { StartTime = 300, Position = new Vector2(200) },
|
||||||
|
new HitCircle { StartTime = 400, Position = new Vector2(300) },
|
||||||
|
new HitCircle { StartTime = 500, Position = new Vector2(400) },
|
||||||
|
};
|
||||||
|
|
||||||
|
AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects));
|
||||||
|
|
||||||
|
moveMouseToObject(() => addedObjects[1]);
|
||||||
|
AddStep("click second", () => InputManager.Click(MouseButton.Left));
|
||||||
|
|
||||||
|
AddAssert("hitobject selected", () => EditorBeatmap.SelectedHitObjects.Single() == addedObjects[1]);
|
||||||
|
|
||||||
|
AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft));
|
||||||
|
|
||||||
|
moveMouseToObject(() => addedObjects[3]);
|
||||||
|
AddStep("click fourth", () => InputManager.Click(MouseButton.Left));
|
||||||
|
assertSelectionIs(addedObjects.Skip(1).Take(3));
|
||||||
|
|
||||||
|
moveMouseToObject(() => addedObjects[0]);
|
||||||
|
AddStep("click first", () => InputManager.Click(MouseButton.Left));
|
||||||
|
assertSelectionIs(addedObjects.Take(2));
|
||||||
|
|
||||||
|
AddStep("clear selection", () => EditorBeatmap.SelectedHitObjects.Clear());
|
||||||
|
AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
|
||||||
|
|
||||||
|
moveMouseToObject(() => addedObjects[0]);
|
||||||
|
AddStep("click first", () => InputManager.Click(MouseButton.Left));
|
||||||
|
assertSelectionIs(addedObjects.Take(1));
|
||||||
|
|
||||||
|
AddStep("hold ctrl", () => InputManager.PressKey(Key.ControlLeft));
|
||||||
|
moveMouseToObject(() => addedObjects[2]);
|
||||||
|
AddStep("click third", () => InputManager.Click(MouseButton.Left));
|
||||||
|
assertSelectionIs(new[] { addedObjects[0], addedObjects[2] });
|
||||||
|
|
||||||
|
AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft));
|
||||||
|
moveMouseToObject(() => addedObjects[4]);
|
||||||
|
AddStep("click fifth", () => InputManager.Click(MouseButton.Left));
|
||||||
|
assertSelectionIs(addedObjects.Except(new[] { addedObjects[1] }));
|
||||||
|
|
||||||
|
moveMouseToObject(() => addedObjects[0]);
|
||||||
|
AddStep("click first", () => InputManager.Click(MouseButton.Left));
|
||||||
|
assertSelectionIs(addedObjects);
|
||||||
|
|
||||||
|
AddStep("clear selection", () => EditorBeatmap.SelectedHitObjects.Clear());
|
||||||
|
moveMouseToObject(() => addedObjects[0]);
|
||||||
|
AddStep("click first", () => InputManager.Click(MouseButton.Left));
|
||||||
|
assertSelectionIs(addedObjects.Take(1));
|
||||||
|
|
||||||
|
moveMouseToObject(() => addedObjects[1]);
|
||||||
|
AddStep("click first", () => InputManager.Click(MouseButton.Left));
|
||||||
|
assertSelectionIs(addedObjects.Take(2));
|
||||||
|
|
||||||
|
moveMouseToObject(() => addedObjects[2]);
|
||||||
|
AddStep("click first", () => InputManager.Click(MouseButton.Left));
|
||||||
|
assertSelectionIs(addedObjects.Take(3));
|
||||||
|
|
||||||
|
AddStep("release keys", () =>
|
||||||
|
{
|
||||||
|
InputManager.ReleaseKey(Key.ControlLeft);
|
||||||
|
InputManager.ReleaseKey(Key.ShiftLeft);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestRangeSelectAfterExternalSelection()
|
||||||
|
{
|
||||||
|
var addedObjects = new[]
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 100 },
|
||||||
|
new HitCircle { StartTime = 200, Position = new Vector2(100) },
|
||||||
|
new HitCircle { StartTime = 300, Position = new Vector2(200) },
|
||||||
|
new HitCircle { StartTime = 400, Position = new Vector2(300) },
|
||||||
|
};
|
||||||
|
|
||||||
|
AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects));
|
||||||
|
|
||||||
|
AddStep("select all without mouse", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
|
||||||
|
assertSelectionIs(addedObjects);
|
||||||
|
|
||||||
|
AddStep("hold down shift", () => InputManager.PressKey(Key.ShiftLeft));
|
||||||
|
|
||||||
|
moveMouseToObject(() => addedObjects[1]);
|
||||||
|
AddStep("click second object", () => InputManager.Click(MouseButton.Left));
|
||||||
|
assertSelectionIs(addedObjects);
|
||||||
|
|
||||||
|
moveMouseToObject(() => addedObjects[3]);
|
||||||
|
AddStep("click fourth object", () => InputManager.Click(MouseButton.Left));
|
||||||
|
assertSelectionIs(addedObjects.Skip(1));
|
||||||
|
|
||||||
|
AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertSelectionIs(IEnumerable<HitObject> hitObjects)
|
||||||
|
=> AddAssert("correct hitobjects selected", () => EditorBeatmap.SelectedHitObjects.OrderBy(h => h.StartTime).SequenceEqual(hitObjects));
|
||||||
|
}
|
||||||
|
}
|
@ -3,10 +3,12 @@
|
|||||||
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Screens;
|
using osu.Framework.Screens;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
using osu.Game.Graphics.Cursor;
|
using osu.Game.Graphics.Cursor;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
@ -39,6 +41,45 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
confirmClockRunning(true);
|
confirmClockRunning(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPauseWithLargeOffset()
|
||||||
|
{
|
||||||
|
double lastTime;
|
||||||
|
bool alwaysGoingForward = true;
|
||||||
|
|
||||||
|
AddStep("force large offset", () =>
|
||||||
|
{
|
||||||
|
var offset = (BindableDouble)LocalConfig.GetBindable<double>(OsuSetting.AudioOffset);
|
||||||
|
|
||||||
|
// use a large negative offset to avoid triggering a fail from forwards seeking.
|
||||||
|
offset.MinValue = -5000;
|
||||||
|
offset.Value = -5000;
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("add time forward check hook", () =>
|
||||||
|
{
|
||||||
|
lastTime = double.MinValue;
|
||||||
|
alwaysGoingForward = true;
|
||||||
|
|
||||||
|
Player.OnUpdate += _ =>
|
||||||
|
{
|
||||||
|
double currentTime = Player.GameplayClockContainer.CurrentTime;
|
||||||
|
alwaysGoingForward &= currentTime >= lastTime;
|
||||||
|
lastTime = currentTime;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("move cursor outside", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopLeft - new Vector2(10)));
|
||||||
|
|
||||||
|
pauseAndConfirm();
|
||||||
|
|
||||||
|
resumeAndConfirm();
|
||||||
|
|
||||||
|
AddAssert("time didn't go backwards", () => alwaysGoingForward);
|
||||||
|
|
||||||
|
AddStep("reset offset", () => LocalConfig.SetValue(OsuSetting.AudioOffset, 0.0));
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestPauseResume()
|
public void TestPauseResume()
|
||||||
{
|
{
|
||||||
|
@ -24,9 +24,10 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestBasic()
|
public void TestBasic()
|
||||||
{
|
{
|
||||||
TestPopupDialog dialog = null;
|
TestPopupDialog firstDialog = null;
|
||||||
|
TestPopupDialog secondDialog = null;
|
||||||
|
|
||||||
AddStep("dialog #1", () => overlay.Push(dialog = new TestPopupDialog
|
AddStep("dialog #1", () => overlay.Push(firstDialog = new TestPopupDialog
|
||||||
{
|
{
|
||||||
Icon = FontAwesome.Regular.TrashAlt,
|
Icon = FontAwesome.Regular.TrashAlt,
|
||||||
HeaderText = @"Confirm deletion of",
|
HeaderText = @"Confirm deletion of",
|
||||||
@ -46,9 +47,9 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
AddAssert("first dialog displayed", () => overlay.CurrentDialog == dialog);
|
AddAssert("first dialog displayed", () => overlay.CurrentDialog == firstDialog);
|
||||||
|
|
||||||
AddStep("dialog #2", () => overlay.Push(dialog = new TestPopupDialog
|
AddStep("dialog #2", () => overlay.Push(secondDialog = new TestPopupDialog
|
||||||
{
|
{
|
||||||
Icon = FontAwesome.Solid.Cog,
|
Icon = FontAwesome.Solid.Cog,
|
||||||
HeaderText = @"What do you want to do with",
|
HeaderText = @"What do you want to do with",
|
||||||
@ -82,30 +83,33 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
AddAssert("second dialog displayed", () => overlay.CurrentDialog == dialog);
|
AddAssert("second dialog displayed", () => overlay.CurrentDialog == secondDialog);
|
||||||
|
AddAssert("first dialog is not part of hierarchy", () => firstDialog.Parent == null);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestDismissBeforePush()
|
public void TestDismissBeforePush()
|
||||||
{
|
{
|
||||||
|
TestPopupDialog testDialog = null;
|
||||||
AddStep("dismissed dialog push", () =>
|
AddStep("dismissed dialog push", () =>
|
||||||
{
|
{
|
||||||
overlay.Push(new TestPopupDialog
|
overlay.Push(testDialog = new TestPopupDialog
|
||||||
{
|
{
|
||||||
State = { Value = Visibility.Hidden }
|
State = { Value = Visibility.Hidden }
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
AddAssert("no dialog pushed", () => overlay.CurrentDialog == null);
|
AddAssert("no dialog pushed", () => overlay.CurrentDialog == null);
|
||||||
|
AddAssert("dialog is not part of hierarchy", () => testDialog.Parent == null);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestDismissBeforePushViaButtonPress()
|
public void TestDismissBeforePushViaButtonPress()
|
||||||
{
|
{
|
||||||
|
TestPopupDialog testDialog = null;
|
||||||
AddStep("dismissed dialog push", () =>
|
AddStep("dismissed dialog push", () =>
|
||||||
{
|
{
|
||||||
TestPopupDialog dialog;
|
overlay.Push(testDialog = new TestPopupDialog
|
||||||
overlay.Push(dialog = new TestPopupDialog
|
|
||||||
{
|
{
|
||||||
Buttons = new PopupDialogButton[]
|
Buttons = new PopupDialogButton[]
|
||||||
{
|
{
|
||||||
@ -113,10 +117,11 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
dialog.PerformOkAction();
|
testDialog.PerformOkAction();
|
||||||
});
|
});
|
||||||
|
|
||||||
AddAssert("no dialog pushed", () => overlay.CurrentDialog == null);
|
AddAssert("no dialog pushed", () => overlay.CurrentDialog == null);
|
||||||
|
AddAssert("dialog is not part of hierarchy", () => testDialog.Parent == null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private class TestPopupDialog : PopupDialog
|
private class TestPopupDialog : PopupDialog
|
||||||
|
@ -13,6 +13,7 @@ using osu.Framework.Logging;
|
|||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Framework.Threading;
|
using osu.Framework.Threading;
|
||||||
|
using osu.Game.Database;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Online.API.Requests;
|
using osu.Game.Online.API.Requests;
|
||||||
using SharpCompress.Compressors;
|
using SharpCompress.Compressors;
|
||||||
@ -160,7 +161,7 @@ namespace osu.Game.Beatmaps
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using (var db = new SqliteConnection(storage.GetDatabaseConnectionString("online")))
|
using (var db = new SqliteConnection(DatabaseContextFactory.CreateDatabaseConnectionString("online.db", storage)))
|
||||||
{
|
{
|
||||||
db.Open();
|
db.Open();
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ namespace osu.Game.Database
|
|||||||
{
|
{
|
||||||
private readonly Storage storage;
|
private readonly Storage storage;
|
||||||
|
|
||||||
private const string database_name = @"client";
|
private const string database_name = @"client.db";
|
||||||
|
|
||||||
private ThreadLocal<OsuDbContext> threadContexts;
|
private ThreadLocal<OsuDbContext> threadContexts;
|
||||||
|
|
||||||
@ -139,7 +139,7 @@ namespace osu.Game.Database
|
|||||||
threadContexts = new ThreadLocal<OsuDbContext>(CreateContext, true);
|
threadContexts = new ThreadLocal<OsuDbContext>(CreateContext, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual OsuDbContext CreateContext() => new OsuDbContext(storage.GetDatabaseConnectionString(database_name))
|
protected virtual OsuDbContext CreateContext() => new OsuDbContext(CreateDatabaseConnectionString(database_name, storage))
|
||||||
{
|
{
|
||||||
Database = { AutoTransactionsEnabled = false }
|
Database = { AutoTransactionsEnabled = false }
|
||||||
};
|
};
|
||||||
@ -152,7 +152,7 @@ namespace osu.Game.Database
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
storage.DeleteDatabase(database_name);
|
storage.Delete(database_name);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@ -171,5 +171,7 @@ namespace osu.Game.Database
|
|||||||
|
|
||||||
recycleThreadContexts();
|
recycleThreadContexts();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string CreateDatabaseConnectionString(string filename, Storage storage) => string.Concat("Data Source=", storage.GetFullPath($@"{filename}", true));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -274,21 +274,40 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
CornerRadius = corner_radius;
|
CornerRadius = corner_radius;
|
||||||
Height = 40;
|
Height = 40;
|
||||||
|
|
||||||
Foreground.Children = new Drawable[]
|
Foreground.Child = new GridContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
RowDimensions = new[]
|
||||||
|
{
|
||||||
|
new Dimension(GridSizeMode.AutoSize),
|
||||||
|
},
|
||||||
|
ColumnDimensions = new[]
|
||||||
|
{
|
||||||
|
new Dimension(),
|
||||||
|
new Dimension(GridSizeMode.AutoSize),
|
||||||
|
},
|
||||||
|
Content = new[]
|
||||||
|
{
|
||||||
|
new Drawable[]
|
||||||
{
|
{
|
||||||
Text = new OsuSpriteText
|
Text = new OsuSpriteText
|
||||||
{
|
{
|
||||||
Anchor = Anchor.CentreLeft,
|
Anchor = Anchor.CentreLeft,
|
||||||
Origin = Anchor.CentreLeft,
|
Origin = Anchor.CentreLeft,
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
Truncate = true,
|
||||||
},
|
},
|
||||||
Icon = new SpriteIcon
|
Icon = new SpriteIcon
|
||||||
{
|
{
|
||||||
Icon = FontAwesome.Solid.ChevronDown,
|
Icon = FontAwesome.Solid.ChevronDown,
|
||||||
Anchor = Anchor.CentreRight,
|
Anchor = Anchor.CentreRight,
|
||||||
Origin = Anchor.CentreRight,
|
Origin = Anchor.CentreRight,
|
||||||
Margin = new MarginPadding { Right = 5 },
|
Margin = new MarginPadding { Horizontal = 5 },
|
||||||
Size = new Vector2(12),
|
Size = new Vector2(12),
|
||||||
},
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
AddInternal(new HoverClickSounds());
|
AddInternal(new HoverClickSounds());
|
||||||
|
@ -70,11 +70,6 @@ namespace osu.Game.IO
|
|||||||
public override Stream GetStream(string path, FileAccess access = FileAccess.Read, FileMode mode = FileMode.OpenOrCreate) =>
|
public override Stream GetStream(string path, FileAccess access = FileAccess.Read, FileMode mode = FileMode.OpenOrCreate) =>
|
||||||
UnderlyingStorage.GetStream(MutatePath(path), access, mode);
|
UnderlyingStorage.GetStream(MutatePath(path), access, mode);
|
||||||
|
|
||||||
public override string GetDatabaseConnectionString(string name) =>
|
|
||||||
UnderlyingStorage.GetDatabaseConnectionString(MutatePath(name));
|
|
||||||
|
|
||||||
public override void DeleteDatabase(string name) => UnderlyingStorage.DeleteDatabase(MutatePath(name));
|
|
||||||
|
|
||||||
public override void OpenPathInNativeExplorer(string path) => UnderlyingStorage.OpenPathInNativeExplorer(MutatePath(path));
|
public override void OpenPathInNativeExplorer(string path) => UnderlyingStorage.OpenPathInNativeExplorer(MutatePath(path));
|
||||||
|
|
||||||
public override Storage GetStorageForDirectory(string path)
|
public override Storage GetStorageForDirectory(string path)
|
||||||
|
@ -75,6 +75,7 @@ namespace osu.Game.Input.Bindings
|
|||||||
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.A }, GlobalAction.EditorVerifyMode),
|
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.A }, GlobalAction.EditorVerifyMode),
|
||||||
new KeyBinding(new[] { InputKey.J }, GlobalAction.EditorNudgeLeft),
|
new KeyBinding(new[] { InputKey.J }, GlobalAction.EditorNudgeLeft),
|
||||||
new KeyBinding(new[] { InputKey.K }, GlobalAction.EditorNudgeRight),
|
new KeyBinding(new[] { InputKey.K }, GlobalAction.EditorNudgeRight),
|
||||||
|
new KeyBinding(new[] { InputKey.G }, GlobalAction.EditorCycleGridDisplayMode),
|
||||||
};
|
};
|
||||||
|
|
||||||
public IEnumerable<KeyBinding> InGameKeyBindings => new[]
|
public IEnumerable<KeyBinding> InGameKeyBindings => new[]
|
||||||
@ -284,6 +285,9 @@ namespace osu.Game.Input.Bindings
|
|||||||
SeekReplayBackward,
|
SeekReplayBackward,
|
||||||
|
|
||||||
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleChatFocus))]
|
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleChatFocus))]
|
||||||
ToggleChatFocus
|
ToggleChatFocus,
|
||||||
|
|
||||||
|
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorCycleGridDisplayMode))]
|
||||||
|
EditorCycleGridDisplayMode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -164,6 +164,11 @@ namespace osu.Game.Localisation
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static LocalisableString EditorTimingMode => new TranslatableString(getKey(@"editor_timing_mode"), @"Timing mode");
|
public static LocalisableString EditorTimingMode => new TranslatableString(getKey(@"editor_timing_mode"), @"Timing mode");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Cycle grid display mode"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString EditorCycleGridDisplayMode => new TranslatableString(getKey(@"editor_cycle_grid_display_mode"), @"Cycle grid display mode");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// "Hold for HUD"
|
/// "Hold for HUD"
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -49,6 +49,8 @@ namespace osu.Game.Overlays
|
|||||||
Show();
|
Show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override bool IsPresent => dialogContainer.Children.Count > 0;
|
||||||
|
|
||||||
protected override bool BlockNonPositionalInput => true;
|
protected override bool BlockNonPositionalInput => true;
|
||||||
|
|
||||||
private void onDialogOnStateChanged(VisibilityContainer dialog, Visibility v)
|
private void onDialogOnStateChanged(VisibilityContainer dialog, Visibility v)
|
||||||
|
@ -3,11 +3,12 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Extensions.ListExtensions;
|
||||||
|
using osu.Framework.Lists;
|
||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Beatmaps.ControlPoints;
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
@ -83,7 +84,7 @@ namespace osu.Game.Rulesets.Objects
|
|||||||
private readonly List<HitObject> nestedHitObjects = new List<HitObject>();
|
private readonly List<HitObject> nestedHitObjects = new List<HitObject>();
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public IReadOnlyList<HitObject> NestedHitObjects => nestedHitObjects;
|
public SlimReadOnlyListWrapper<HitObject> NestedHitObjects => nestedHitObjects.AsSlimReadOnly();
|
||||||
|
|
||||||
public HitObject()
|
public HitObject()
|
||||||
{
|
{
|
||||||
@ -91,7 +92,7 @@ namespace osu.Game.Rulesets.Objects
|
|||||||
{
|
{
|
||||||
double offset = time.NewValue - time.OldValue;
|
double offset = time.NewValue - time.OldValue;
|
||||||
|
|
||||||
foreach (var nested in NestedHitObjects)
|
foreach (var nested in nestedHitObjects)
|
||||||
nested.StartTime += offset;
|
nested.StartTime += offset;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -122,13 +123,16 @@ namespace osu.Game.Rulesets.Objects
|
|||||||
|
|
||||||
if (this is IHasComboInformation hasCombo)
|
if (this is IHasComboInformation hasCombo)
|
||||||
{
|
{
|
||||||
foreach (var n in NestedHitObjects.OfType<IHasComboInformation>())
|
foreach (HitObject hitObject in nestedHitObjects)
|
||||||
|
{
|
||||||
|
if (hitObject is IHasComboInformation n)
|
||||||
{
|
{
|
||||||
n.ComboIndexBindable.BindTo(hasCombo.ComboIndexBindable);
|
n.ComboIndexBindable.BindTo(hasCombo.ComboIndexBindable);
|
||||||
n.ComboIndexWithOffsetsBindable.BindTo(hasCombo.ComboIndexWithOffsetsBindable);
|
n.ComboIndexWithOffsetsBindable.BindTo(hasCombo.ComboIndexWithOffsetsBindable);
|
||||||
n.IndexInCurrentComboBindable.BindTo(hasCombo.IndexInCurrentComboBindable);
|
n.IndexInCurrentComboBindable.BindTo(hasCombo.IndexInCurrentComboBindable);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
nestedHitObjects.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime));
|
nestedHitObjects.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime));
|
||||||
|
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
|
|
||||||
namespace osu.Game.Screens.Edit.Compose.Components
|
namespace osu.Game.Screens.Edit.Compose.Components
|
||||||
{
|
{
|
||||||
@ -15,69 +15,62 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class HitObjectOrderedSelectionContainer : Container<SelectionBlueprint<HitObject>>
|
public sealed class HitObjectOrderedSelectionContainer : Container<SelectionBlueprint<HitObject>>
|
||||||
{
|
{
|
||||||
|
[Resolved]
|
||||||
|
private EditorBeatmap editorBeatmap { get; set; }
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
editorBeatmap.HitObjectUpdated += hitObjectUpdated;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hitObjectUpdated(HitObject _) => SortInternal();
|
||||||
|
|
||||||
public override void Add(SelectionBlueprint<HitObject> drawable)
|
public override void Add(SelectionBlueprint<HitObject> drawable)
|
||||||
{
|
{
|
||||||
|
SortInternal();
|
||||||
base.Add(drawable);
|
base.Add(drawable);
|
||||||
bindStartTime(drawable);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool Remove(SelectionBlueprint<HitObject> drawable)
|
public override bool Remove(SelectionBlueprint<HitObject> drawable)
|
||||||
{
|
{
|
||||||
if (!base.Remove(drawable))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
unbindStartTime(drawable);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Clear(bool disposeChildren)
|
|
||||||
{
|
|
||||||
base.Clear(disposeChildren);
|
|
||||||
unbindAllStartTimes();
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly Dictionary<SelectionBlueprint<HitObject>, IBindable> startTimeMap = new Dictionary<SelectionBlueprint<HitObject>, IBindable>();
|
|
||||||
|
|
||||||
private void bindStartTime(SelectionBlueprint<HitObject> blueprint)
|
|
||||||
{
|
|
||||||
var bindable = blueprint.Item.StartTimeBindable.GetBoundCopy();
|
|
||||||
|
|
||||||
bindable.BindValueChanged(_ =>
|
|
||||||
{
|
|
||||||
if (LoadState >= LoadState.Ready)
|
|
||||||
SortInternal();
|
SortInternal();
|
||||||
});
|
return base.Remove(drawable);
|
||||||
|
|
||||||
startTimeMap[blueprint] = bindable;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void unbindStartTime(SelectionBlueprint<HitObject> blueprint)
|
|
||||||
{
|
|
||||||
startTimeMap[blueprint].UnbindAll();
|
|
||||||
startTimeMap.Remove(blueprint);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void unbindAllStartTimes()
|
|
||||||
{
|
|
||||||
foreach (var kvp in startTimeMap)
|
|
||||||
kvp.Value.UnbindAll();
|
|
||||||
startTimeMap.Clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override int Compare(Drawable x, Drawable y)
|
protected override int Compare(Drawable x, Drawable y)
|
||||||
{
|
{
|
||||||
var xObj = (SelectionBlueprint<HitObject>)x;
|
var xObj = ((SelectionBlueprint<HitObject>)x).Item;
|
||||||
var yObj = (SelectionBlueprint<HitObject>)y;
|
var yObj = ((SelectionBlueprint<HitObject>)y).Item;
|
||||||
|
|
||||||
// Put earlier blueprints towards the end of the list, so they handle input first
|
// Put earlier blueprints towards the end of the list, so they handle input first
|
||||||
int i = yObj.Item.StartTime.CompareTo(xObj.Item.StartTime);
|
int result = yObj.StartTime.CompareTo(xObj.StartTime);
|
||||||
|
if (result != 0) return result;
|
||||||
if (i != 0) return i;
|
|
||||||
|
|
||||||
// Fall back to end time if the start time is equal.
|
// Fall back to end time if the start time is equal.
|
||||||
i = yObj.Item.GetEndTime().CompareTo(xObj.Item.GetEndTime());
|
result = yObj.GetEndTime().CompareTo(xObj.GetEndTime());
|
||||||
|
if (result != 0) return result;
|
||||||
|
|
||||||
return i == 0 ? CompareReverseChildID(y, x) : i;
|
// As a final fallback, use combo information if available.
|
||||||
|
if (xObj is IHasComboInformation xHasCombo && yObj is IHasComboInformation yHasCombo)
|
||||||
|
{
|
||||||
|
result = yHasCombo.ComboIndex.CompareTo(xHasCombo.ComboIndex);
|
||||||
|
if (result != 0) return result;
|
||||||
|
|
||||||
|
result = yHasCombo.IndexInCurrentCombo.CompareTo(xHasCombo.IndexInCurrentCombo);
|
||||||
|
if (result != 0) return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CompareReverseChildID(y, x);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool isDisposing)
|
||||||
|
{
|
||||||
|
base.Dispose(isDisposing);
|
||||||
|
|
||||||
|
if (editorBeatmap != null)
|
||||||
|
editorBeatmap.HitObjectUpdated -= hitObjectUpdated;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,113 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Framework.Layout;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.Edit.Compose.Components
|
||||||
|
{
|
||||||
|
public class RectangularPositionSnapGrid : CompositeDrawable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The position of the origin of this <see cref="RectangularPositionSnapGrid"/> in local coordinates.
|
||||||
|
/// </summary>
|
||||||
|
public Vector2 StartPosition { get; }
|
||||||
|
|
||||||
|
private Vector2 spacing = Vector2.One;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The spacing between grid lines of this <see cref="RectangularPositionSnapGrid"/>.
|
||||||
|
/// </summary>
|
||||||
|
public Vector2 Spacing
|
||||||
|
{
|
||||||
|
get => spacing;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (spacing.X <= 0 || spacing.Y <= 0)
|
||||||
|
throw new ArgumentException("Grid spacing must be positive.");
|
||||||
|
|
||||||
|
spacing = value;
|
||||||
|
gridCache.Invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly LayoutValue gridCache = new LayoutValue(Invalidation.RequiredParentSizeToFit);
|
||||||
|
|
||||||
|
public RectangularPositionSnapGrid(Vector2 startPosition)
|
||||||
|
{
|
||||||
|
StartPosition = startPosition;
|
||||||
|
|
||||||
|
AddLayout(gridCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
base.Update();
|
||||||
|
|
||||||
|
if (!gridCache.IsValid)
|
||||||
|
{
|
||||||
|
ClearInternal();
|
||||||
|
createContent();
|
||||||
|
gridCache.Validate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createContent()
|
||||||
|
{
|
||||||
|
var drawSize = DrawSize;
|
||||||
|
|
||||||
|
generateGridLines(Direction.Horizontal, StartPosition.Y, 0, -Spacing.Y);
|
||||||
|
generateGridLines(Direction.Horizontal, StartPosition.Y, drawSize.Y, Spacing.Y);
|
||||||
|
|
||||||
|
generateGridLines(Direction.Vertical, StartPosition.X, 0, -Spacing.X);
|
||||||
|
generateGridLines(Direction.Vertical, StartPosition.X, drawSize.X, Spacing.X);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void generateGridLines(Direction direction, float startPosition, float endPosition, float step)
|
||||||
|
{
|
||||||
|
int index = 0;
|
||||||
|
float currentPosition = startPosition;
|
||||||
|
|
||||||
|
while ((endPosition - currentPosition) * Math.Sign(step) > 0)
|
||||||
|
{
|
||||||
|
var gridLine = new Box
|
||||||
|
{
|
||||||
|
Colour = Colour4.White,
|
||||||
|
Alpha = index == 0 ? 0.3f : 0.1f,
|
||||||
|
EdgeSmoothness = new Vector2(0.2f)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (direction == Direction.Horizontal)
|
||||||
|
{
|
||||||
|
gridLine.RelativeSizeAxes = Axes.X;
|
||||||
|
gridLine.Height = 1;
|
||||||
|
gridLine.Y = currentPosition;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
gridLine.RelativeSizeAxes = Axes.Y;
|
||||||
|
gridLine.Width = 1;
|
||||||
|
gridLine.X = currentPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
AddInternal(gridLine);
|
||||||
|
|
||||||
|
index += 1;
|
||||||
|
currentPosition = startPosition + index * step;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector2 GetSnappedPosition(Vector2 original)
|
||||||
|
{
|
||||||
|
Vector2 relativeToStart = original - StartPosition;
|
||||||
|
Vector2 offset = Vector2.Divide(relativeToStart, Spacing);
|
||||||
|
Vector2 roundedOffset = new Vector2(MathF.Round(offset.X), MathF.Round(offset.Y));
|
||||||
|
|
||||||
|
return StartPosition + Vector2.Multiply(roundedOffset, Spacing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -191,7 +191,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
/// <param name="blueprint">The blueprint.</param>
|
/// <param name="blueprint">The blueprint.</param>
|
||||||
/// <param name="e">The mouse event responsible for selection.</param>
|
/// <param name="e">The mouse event responsible for selection.</param>
|
||||||
/// <returns>Whether a selection was performed.</returns>
|
/// <returns>Whether a selection was performed.</returns>
|
||||||
internal bool MouseDownSelectionRequested(SelectionBlueprint<T> blueprint, MouseButtonEvent e)
|
internal virtual bool MouseDownSelectionRequested(SelectionBlueprint<T> blueprint, MouseButtonEvent e)
|
||||||
{
|
{
|
||||||
if (e.ShiftPressed && e.Button == MouseButton.Right)
|
if (e.ShiftPressed && e.Button == MouseButton.Right)
|
||||||
{
|
{
|
||||||
|
@ -1,13 +1,19 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Input.Bindings;
|
using osu.Framework.Input.Bindings;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Game.Input.Bindings;
|
using osu.Game.Input.Bindings;
|
||||||
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||||
{
|
{
|
||||||
@ -62,5 +68,62 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
EditorBeatmap.Update(h);
|
EditorBeatmap.Update(h);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The "pivot" object, used in range selection mode.
|
||||||
|
/// When in range selection, the range to select is determined by the pivot object
|
||||||
|
/// (last existing object interacted with prior to holding down Shift)
|
||||||
|
/// and by the object clicked last when Shift was pressed.
|
||||||
|
/// </summary>
|
||||||
|
[CanBeNull]
|
||||||
|
private HitObject pivot;
|
||||||
|
|
||||||
|
internal override bool MouseDownSelectionRequested(SelectionBlueprint<HitObject> blueprint, MouseButtonEvent e)
|
||||||
|
{
|
||||||
|
if (e.ShiftPressed && e.Button == MouseButton.Left && pivot != null)
|
||||||
|
{
|
||||||
|
handleRangeSelection(blueprint, e.ControlPressed);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool result = base.MouseDownSelectionRequested(blueprint, e);
|
||||||
|
// ensure that the object wasn't removed by the base implementation before making it the new pivot.
|
||||||
|
if (EditorBeatmap.HitObjects.Contains(blueprint.Item))
|
||||||
|
pivot = blueprint.Item;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles a request for range selection (triggered when Shift is held down).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="blueprint">The blueprint which was clicked in range selection mode.</param>
|
||||||
|
/// <param name="cumulative">
|
||||||
|
/// Whether the selection should be cumulative.
|
||||||
|
/// In cumulative mode, consecutive range selections will shift the pivot (which usually stays fixed for the duration of a range selection)
|
||||||
|
/// and will never deselect an object that was previously selected.
|
||||||
|
/// </param>
|
||||||
|
private void handleRangeSelection(SelectionBlueprint<HitObject> blueprint, bool cumulative)
|
||||||
|
{
|
||||||
|
var clickedObject = blueprint.Item;
|
||||||
|
|
||||||
|
Debug.Assert(pivot != null);
|
||||||
|
|
||||||
|
double rangeStart = Math.Min(clickedObject.StartTime, pivot.StartTime);
|
||||||
|
double rangeEnd = Math.Max(clickedObject.GetEndTime(), pivot.GetEndTime());
|
||||||
|
|
||||||
|
var newSelection = new HashSet<HitObject>(EditorBeatmap.HitObjects.Where(obj => isInRange(obj, rangeStart, rangeEnd)));
|
||||||
|
|
||||||
|
if (cumulative)
|
||||||
|
{
|
||||||
|
pivot = clickedObject;
|
||||||
|
newSelection.UnionWith(EditorBeatmap.SelectedHitObjects);
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorBeatmap.SelectedHitObjects.Clear();
|
||||||
|
EditorBeatmap.SelectedHitObjects.AddRange(newSelection);
|
||||||
|
|
||||||
|
bool isInRange(HitObject hitObject, double start, double end)
|
||||||
|
=> hitObject.StartTime >= start && hitObject.GetEndTime() <= end;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -158,10 +158,10 @@ namespace osu.Game.Screens.Play
|
|||||||
{
|
{
|
||||||
// Lazer's audio timings in general doesn't match stable. This is the result of user testing, albeit limited.
|
// Lazer's audio timings in general doesn't match stable. This is the result of user testing, albeit limited.
|
||||||
// This only seems to be required on windows. We need to eventually figure out why, with a bit of luck.
|
// This only seems to be required on windows. We need to eventually figure out why, with a bit of luck.
|
||||||
platformOffsetClock = new HardwareCorrectionOffsetClock(source) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 };
|
platformOffsetClock = new HardwareCorrectionOffsetClock(source, pauseFreqAdjust) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 };
|
||||||
|
|
||||||
// the final usable gameplay clock with user-set offsets applied.
|
// the final usable gameplay clock with user-set offsets applied.
|
||||||
userOffsetClock = new HardwareCorrectionOffsetClock(platformOffsetClock);
|
userOffsetClock = new HardwareCorrectionOffsetClock(platformOffsetClock, pauseFreqAdjust);
|
||||||
|
|
||||||
return masterGameplayClock = new MasterGameplayClock(userOffsetClock);
|
return masterGameplayClock = new MasterGameplayClock(userOffsetClock);
|
||||||
}
|
}
|
||||||
@ -216,11 +216,25 @@ namespace osu.Game.Screens.Play
|
|||||||
{
|
{
|
||||||
// we always want to apply the same real-time offset, so it should be adjusted by the difference in playback rate (from realtime) to achieve this.
|
// we always want to apply the same real-time offset, so it should be adjusted by the difference in playback rate (from realtime) to achieve this.
|
||||||
// base implementation already adds offset at 1.0 rate, so we only add the difference from that here.
|
// base implementation already adds offset at 1.0 rate, so we only add the difference from that here.
|
||||||
public override double CurrentTime => base.CurrentTime + Offset * (Rate - 1);
|
public override double CurrentTime => base.CurrentTime + offsetAdjust;
|
||||||
|
|
||||||
public HardwareCorrectionOffsetClock(IClock source, bool processSource = true)
|
private readonly BindableDouble pauseRateAdjust;
|
||||||
: base(source, processSource)
|
|
||||||
|
private double offsetAdjust;
|
||||||
|
|
||||||
|
public HardwareCorrectionOffsetClock(IClock source, BindableDouble pauseRateAdjust)
|
||||||
|
: base(source)
|
||||||
{
|
{
|
||||||
|
this.pauseRateAdjust = pauseRateAdjust;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void ProcessFrame()
|
||||||
|
{
|
||||||
|
base.ProcessFrame();
|
||||||
|
|
||||||
|
// changing this during the pause transform effect will cause a potentially large offset to be suddenly applied as we approach zero rate.
|
||||||
|
if (pauseRateAdjust.Value == 1)
|
||||||
|
offsetAdjust = Offset * (Rate - 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@
|
|||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Realm" Version="10.5.0" />
|
<PackageReference Include="Realm" Version="10.5.0" />
|
||||||
<PackageReference Include="ppy.osu.Framework" Version="2021.916.1" />
|
<PackageReference Include="ppy.osu.Framework" Version="2021.929.0" />
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.918.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.918.0" />
|
||||||
<PackageReference Include="Sentry" Version="3.9.0" />
|
<PackageReference Include="Sentry" Version="3.9.0" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.29.0" />
|
<PackageReference Include="SharpCompress" Version="0.29.0" />
|
||||||
|
@ -70,7 +70,7 @@
|
|||||||
<Reference Include="System.Net.Http" />
|
<Reference Include="System.Net.Http" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.916.1" />
|
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.929.0" />
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.918.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.918.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
|
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
|
||||||
@ -93,7 +93,7 @@
|
|||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||||
<PackageReference Include="ppy.osu.Framework" Version="2021.916.1" />
|
<PackageReference Include="ppy.osu.Framework" Version="2021.929.0" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.28.3" />
|
<PackageReference Include="SharpCompress" Version="0.28.3" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||||
<PackageReference Include="SharpRaven" Version="2.4.0" />
|
<PackageReference Include="SharpRaven" Version="2.4.0" />
|
||||||
|
Loading…
Reference in New Issue
Block a user