mirror of
https://github.com/ppy/osu.git
synced 2026-05-25 03:49:55 +08:00
Merge branch 'master' into add-mod-search-option
This commit is contained in:
@@ -191,6 +191,8 @@ csharp_style_prefer_index_operator = false:silent
|
||||
csharp_style_prefer_range_operator = false:silent
|
||||
csharp_style_prefer_switch_expression = false:none
|
||||
|
||||
csharp_style_namespace_declarations = block_scoped:warning
|
||||
|
||||
[*.{yaml,yml}]
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
|
||||
@@ -121,12 +121,24 @@ jobs:
|
||||
|
||||
build-only-ios:
|
||||
name: Build only (iOS)
|
||||
runs-on: macos-latest
|
||||
# `macos-13` is required, because Xcode 14.3 is required (see below).
|
||||
# TODO: can be changed to `macos-latest` once `macos-13` becomes latest (currently in beta)
|
||||
runs-on: macos-13
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# newest Microsoft.iOS.Sdk versions require Xcode 14.3.
|
||||
# 14.3 is currently not the default Xcode version (https://github.com/actions/runner-images/blob/main/images/macos/macos-13-Readme.md#xcode),
|
||||
# so set it manually.
|
||||
# TODO: remove when 14.3 becomes the default Xcode version.
|
||||
- name: Set Xcode version
|
||||
shell: bash
|
||||
run: |
|
||||
sudo xcode-select -s "/Applications/Xcode_14.3.app"
|
||||
echo "MD_APPLE_SDK_ROOT=/Applications/Xcode_14.3.app" >> $GITHUB_ENV
|
||||
|
||||
- name: Install .NET 6.0.x
|
||||
uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
name: Update osu-web mod definitions
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
jobs:
|
||||
update-mod-definitions:
|
||||
name: Update osu-web mod definitions
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install .NET 6.0.x
|
||||
uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: "6.0.x"
|
||||
|
||||
- name: Checkout ppy/osu
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: osu
|
||||
|
||||
- name: Checkout ppy/osu-tools
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: ppy/osu-tools
|
||||
path: osu-tools
|
||||
|
||||
- name: Checkout ppy/osu-web
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: ppy/osu-web
|
||||
path: osu-web
|
||||
|
||||
- name: Setup local game checkout for tools
|
||||
run: ./UseLocalOsu.sh
|
||||
working-directory: ./osu-tools
|
||||
|
||||
- name: Regenerate mod definitions
|
||||
run: dotnet run --project PerformanceCalculator -- mods > ../osu-web/database/mods.json
|
||||
working-directory: ./osu-tools
|
||||
|
||||
- name: Create pull request with changes
|
||||
uses: peter-evans/create-pull-request@v5
|
||||
with:
|
||||
title: Update mod definitions
|
||||
body: "This PR has been auto-generated to update the mod definitions to match ppy/osu@${{ github.ref_name }}."
|
||||
branch: update-mod-definitions
|
||||
commit-message: Update mod definitions
|
||||
path: osu-web
|
||||
token: ${{ secrets.OSU_WEB_PULL_REQUEST_PAT }}
|
||||
+1
-2
@@ -8,7 +8,6 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osuTK;
|
||||
@@ -44,7 +43,7 @@ namespace osu.Game.Rulesets.Pippidon.Objects.Drawables
|
||||
|
||||
public override IEnumerable<HitSampleInfo> GetSamples() => new[]
|
||||
{
|
||||
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK)
|
||||
new HitSampleInfo(HitSampleInfo.HIT_NORMAL)
|
||||
};
|
||||
|
||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||
|
||||
+1
-2
@@ -8,7 +8,6 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Pippidon.UI;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
@@ -44,7 +43,7 @@ namespace osu.Game.Rulesets.Pippidon.Objects.Drawables
|
||||
|
||||
public override IEnumerable<HitSampleInfo> GetSamples() => new[]
|
||||
{
|
||||
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK)
|
||||
new HitSampleInfo(HitSampleInfo.HIT_NORMAL)
|
||||
};
|
||||
|
||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "6.0.100",
|
||||
"rollForward": "latestFeature"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
<AndroidManifestMerger>manifestmerger.jar</AndroidManifestMerger>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.418.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.521.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidManifestOverlay Include="$(MSBuildThisFileDirectory)osu.Android\Properties\AndroidManifestOverlay.xml" />
|
||||
|
||||
@@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
AddAssert("end time is correct", () => Precision.AlmostEquals(lastObject.EndTime, times[1]));
|
||||
AddAssert("start position is correct", () => Precision.AlmostEquals(lastObject.OriginalX, positions[0]));
|
||||
AddAssert("end position is correct", () => Precision.AlmostEquals(lastObject.EndX, positions[1]));
|
||||
AddAssert("default slider velocity", () => lastObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
|
||||
AddAssert("default slider velocity", () => lastObject.SliderVelocityBindable.IsDefault);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
addPlacementSteps(times, positions);
|
||||
addPathCheckStep(times, positions);
|
||||
|
||||
AddAssert("slider velocity changed", () => !lastObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
|
||||
AddAssert("slider velocity changed", () => !lastObject.SliderVelocityBindable.IsDefault);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -108,11 +108,11 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
double[] times = { 100, 300 };
|
||||
float[] positions = { 200, 300 };
|
||||
addBlueprintStep(times, positions);
|
||||
AddAssert("default slider velocity", () => hitObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
|
||||
AddAssert("default slider velocity", () => hitObject.SliderVelocityBindable.IsDefault);
|
||||
|
||||
addDragStartStep(times[1], positions[1]);
|
||||
AddMouseMoveStep(times[1], 400);
|
||||
AddAssert("slider velocity changed", () => !hitObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
|
||||
AddAssert("slider velocity changed", () => !hitObject.SliderVelocityBindable.IsDefault);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
NewCombo = i % 8 == 0,
|
||||
Samples = new List<HitSampleInfo>(new[]
|
||||
{
|
||||
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "normal", volume: 100)
|
||||
new HitSampleInfo(HitSampleInfo.HIT_NORMAL)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
var xPositionData = obj as IHasXPosition;
|
||||
var yPositionData = obj as IHasYPosition;
|
||||
var comboData = obj as IHasCombo;
|
||||
var sliderVelocityData = obj as IHasSliderVelocity;
|
||||
|
||||
switch (obj)
|
||||
{
|
||||
@@ -41,7 +42,8 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
NewCombo = comboData?.NewCombo ?? false,
|
||||
ComboOffset = comboData?.ComboOffset ?? 0,
|
||||
LegacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0,
|
||||
LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y
|
||||
LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y,
|
||||
SliderVelocity = sliderVelocityData?.SliderVelocity ?? 1
|
||||
}.Yield();
|
||||
|
||||
case IHasDuration endTime:
|
||||
|
||||
@@ -17,6 +17,8 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
private double placementStartTime;
|
||||
private double placementEndTime;
|
||||
|
||||
protected override bool IsValidForPlacement => HitObject.Duration > 0;
|
||||
|
||||
public BananaShowerPlacementBlueprint()
|
||||
{
|
||||
InternalChild = outline = new TimeSpanOutline();
|
||||
@@ -49,7 +51,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
case PlacementState.Active:
|
||||
if (e.Button != MouseButton.Right) break;
|
||||
|
||||
EndPlacement(HitObject.Duration > 0);
|
||||
EndPlacement(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
public void UpdateHitObjectFromPath(JuiceStream hitObject)
|
||||
{
|
||||
// The SV setting may need to be changed for the current path.
|
||||
var svBindable = hitObject.DifficultyControlPoint.SliderVelocityBindable;
|
||||
var svBindable = hitObject.SliderVelocityBindable;
|
||||
double svToVelocityFactor = hitObject.Velocity / svBindable.Value;
|
||||
double requiredVelocity = path.ComputeRequiredVelocity();
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
|
||||
private InputManager inputManager = null!;
|
||||
|
||||
protected override bool IsValidForPlacement => HitObject.Duration > 0;
|
||||
|
||||
public JuiceStreamPlacementBlueprint()
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
@@ -70,7 +72,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
return true;
|
||||
|
||||
case MouseButton.Right:
|
||||
EndPlacement(HitObject.Duration > 0);
|
||||
EndPlacement(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,11 +22,11 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
|
||||
public override Judgement CreateJudgement() => new CatchBananaJudgement();
|
||||
|
||||
private static readonly List<HitSampleInfo> samples = new List<HitSampleInfo> { new BananaHitSampleInfo() };
|
||||
private static readonly IList<HitSampleInfo> default_banana_samples = new List<HitSampleInfo> { new BananaHitSampleInfo() }.AsReadOnly();
|
||||
|
||||
public Banana()
|
||||
{
|
||||
Samples = samples;
|
||||
Samples = default_banana_samples;
|
||||
}
|
||||
|
||||
// override any external colour changes with banananana
|
||||
@@ -47,18 +47,18 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
}
|
||||
}
|
||||
|
||||
private class BananaHitSampleInfo : HitSampleInfo, IEquatable<BananaHitSampleInfo>
|
||||
public class BananaHitSampleInfo : HitSampleInfo, IEquatable<BananaHitSampleInfo>
|
||||
{
|
||||
private static readonly string[] lookup_names = { "Gameplay/metronomelow", "Gameplay/catch-banana" };
|
||||
|
||||
public override IEnumerable<string> LookupNames => lookup_names;
|
||||
|
||||
public BananaHitSampleInfo(int volume = 0)
|
||||
public BananaHitSampleInfo(int volume = 100)
|
||||
: base(string.Empty, volume: volume)
|
||||
{
|
||||
}
|
||||
|
||||
public sealed override HitSampleInfo With(Optional<string> newName = default, Optional<string?> newBank = default, Optional<string?> newSuffix = default, Optional<int> newVolume = default)
|
||||
public sealed override HitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<string?> newSuffix = default, Optional<int> newVolume = default)
|
||||
=> new BananaHitSampleInfo(newVolume.GetOr(Volume));
|
||||
|
||||
public bool Equals(BananaHitSampleInfo? other)
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Threading;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
|
||||
@@ -39,6 +41,7 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
{
|
||||
StartTime = time,
|
||||
BananaIndex = i,
|
||||
Samples = new List<HitSampleInfo> { new Banana.BananaHitSampleInfo(CreateHitSampleInfo().Volume) }
|
||||
});
|
||||
|
||||
time += spacing;
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
@@ -16,7 +17,7 @@ using osu.Game.Rulesets.Objects.Types;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Objects
|
||||
{
|
||||
public class JuiceStream : CatchHitObject, IHasPathWithRepeats
|
||||
public class JuiceStream : CatchHitObject, IHasPathWithRepeats, IHasSliderVelocity
|
||||
{
|
||||
/// <summary>
|
||||
/// Positional distance that results in a duration of one second, before any speed adjustments.
|
||||
@@ -27,6 +28,19 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
|
||||
public int RepeatCount { get; set; }
|
||||
|
||||
public BindableNumber<double> SliderVelocityBindable { get; } = new BindableDouble(1)
|
||||
{
|
||||
Precision = 0.01,
|
||||
MinValue = 0.1,
|
||||
MaxValue = 10
|
||||
};
|
||||
|
||||
public double SliderVelocity
|
||||
{
|
||||
get => SliderVelocityBindable.Value;
|
||||
set => SliderVelocityBindable.Value = value;
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
private double velocityFactor;
|
||||
|
||||
@@ -34,10 +48,10 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
private double tickDistanceFactor;
|
||||
|
||||
[JsonIgnore]
|
||||
public double Velocity => velocityFactor * DifficultyControlPoint.SliderVelocity;
|
||||
public double Velocity => velocityFactor * SliderVelocity;
|
||||
|
||||
[JsonIgnore]
|
||||
public double TickDistance => tickDistanceFactor * DifficultyControlPoint.SliderVelocity;
|
||||
public double TickDistance => tickDistanceFactor * SliderVelocity;
|
||||
|
||||
/// <summary>
|
||||
/// The length of one span of this <see cref="JuiceStream"/>.
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
@@ -40,7 +38,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
private const double time_tail = 4000;
|
||||
private const double time_after_tail = 5250;
|
||||
|
||||
private List<JudgementResult> judgementResults;
|
||||
private List<JudgementResult> judgementResults = new List<JudgementResult>();
|
||||
|
||||
/// <summary>
|
||||
/// -----[ ]-----
|
||||
@@ -61,6 +59,44 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
assertNoteJudgement(HitResult.IgnoreMiss);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// -----[ ]-----
|
||||
/// x o
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestCorrectInput()
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(time_head, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(time_tail),
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Perfect);
|
||||
assertTickJudgement(HitResult.LargeTickHit);
|
||||
assertTailJudgement(HitResult.Perfect);
|
||||
assertNoteJudgement(HitResult.IgnoreHit);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// -----[ ]-----
|
||||
/// x o
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestLateRelease()
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(time_head, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(time_after_tail),
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Perfect);
|
||||
assertTickJudgement(HitResult.LargeTickHit);
|
||||
assertTailJudgement(HitResult.Miss);
|
||||
assertNoteJudgement(HitResult.IgnoreMiss);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// -----[ ]-----
|
||||
/// x o
|
||||
@@ -521,9 +557,9 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
private void assertLastTickJudgement(HitResult result)
|
||||
=> AddAssert($"last tick judged as {result}", () => judgementResults.Last(j => j.HitObject is HoldNoteTick).Type, () => Is.EqualTo(result));
|
||||
|
||||
private ScoreAccessibleReplayPlayer currentPlayer;
|
||||
private ScoreAccessibleReplayPlayer currentPlayer = null!;
|
||||
|
||||
private void performTest(List<ReplayFrame> frames, Beatmap<ManiaHitObject> beatmap = null)
|
||||
private void performTest(List<ReplayFrame> frames, Beatmap<ManiaHitObject>? beatmap = null)
|
||||
{
|
||||
if (beatmap == null)
|
||||
{
|
||||
@@ -569,15 +605,13 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
|
||||
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
|
||||
|
||||
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor?.HasCompleted.Value == true);
|
||||
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
|
||||
}
|
||||
|
||||
private partial class ScoreAccessibleReplayPlayer : ReplayPlayer
|
||||
{
|
||||
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
|
||||
|
||||
public new GameplayClockContainer GameplayClockContainer => base.GameplayClockContainer;
|
||||
|
||||
protected override bool PauseOnFocusLost => false;
|
||||
|
||||
public ScoreAccessibleReplayPlayer(Score score)
|
||||
|
||||
@@ -14,7 +14,6 @@ using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Beatmaps.Formats;
|
||||
using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
@@ -49,15 +48,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
Debug.Assert(distanceData != null);
|
||||
|
||||
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime);
|
||||
DifficultyControlPoint difficultyPoint = hitObject.DifficultyControlPoint;
|
||||
|
||||
double beatLength;
|
||||
#pragma warning disable 618
|
||||
if (difficultyPoint is LegacyBeatmapDecoder.LegacyDifficultyControlPoint legacyDifficultyPoint)
|
||||
#pragma warning restore 618
|
||||
beatLength = timingPoint.BeatLength * legacyDifficultyPoint.BpmMultiplier;
|
||||
if (hitObject.LegacyBpmMultiplier.HasValue)
|
||||
beatLength = timingPoint.BeatLength * hitObject.LegacyBpmMultiplier.Value;
|
||||
else if (hitObject is IHasSliderVelocity hasSliderVelocity)
|
||||
beatLength = timingPoint.BeatLength / hasSliderVelocity.SliderVelocity;
|
||||
else
|
||||
beatLength = timingPoint.BeatLength / difficultyPoint.SliderVelocity;
|
||||
beatLength = timingPoint.BeatLength;
|
||||
|
||||
SpanCount = repeatsData?.SpanCount() ?? 1;
|
||||
StartTime = (int)Math.Round(hitObject.StartTime);
|
||||
|
||||
@@ -25,6 +25,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
[Resolved]
|
||||
private IScrollingInfo scrollingInfo { get; set; }
|
||||
|
||||
protected override bool IsValidForPlacement => HitObject.Duration > 0;
|
||||
|
||||
public HoldNotePlacementBlueprint()
|
||||
: base(new HoldNote())
|
||||
{
|
||||
@@ -75,7 +77,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
return;
|
||||
|
||||
base.OnMouseUp(e);
|
||||
EndPlacement(HitObject.Duration > 0);
|
||||
EndPlacement(true);
|
||||
}
|
||||
|
||||
private double originalStartTime;
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
|
||||
specialStyle = new LabelledSwitchButton
|
||||
{
|
||||
Label = "Use special (N+1) style",
|
||||
Description = "Changes one column to act as a classic \"scratch\" or \"special\" column, which can be moved around by the user's skin (to the left/right/centre). Generally used in 5k (4+1) or 8key (7+1) configurations.",
|
||||
Description = "Changes one column to act as a classic \"scratch\" or \"special\" column, which can be moved around by the user's skin (to the left/right/centre). Generally used in 6K (5+1) or 8K (7+1) configurations.",
|
||||
Current = { Value = Beatmap.BeatmapInfo.SpecialStyle }
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
@@ -219,6 +218,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
if (Time.Current < releaseTime)
|
||||
releaseTime = null;
|
||||
|
||||
if (Time.Current < HoldStartTime)
|
||||
endHold();
|
||||
|
||||
// Pad the full size container so its contents (i.e. the masking container) reach under the tail.
|
||||
// This is required for the tail to not be masked away, since it lies outside the bounds of the hold note.
|
||||
sizingContainer.Padding = new MarginPadding
|
||||
@@ -245,9 +247,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
// 2. The head note will move along with the new "head position" in the container.
|
||||
if (Head.IsHit && releaseTime == null && DrawHeight > 0)
|
||||
{
|
||||
// How far past the hit target this hold note is. Always a positive value.
|
||||
float yOffset = Math.Max(0, Direction.Value == ScrollingDirection.Up ? -Y : Y);
|
||||
sizingContainer.Height = Math.Clamp(1 - yOffset / DrawHeight, 0, 1);
|
||||
// How far past the hit target this hold note is.
|
||||
float yOffset = Direction.Value == ScrollingDirection.Up ? -Y : Y;
|
||||
sizingContainer.Height = 1 - yOffset / DrawHeight;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,14 +324,14 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
if (e.Action != Action.Value)
|
||||
return;
|
||||
|
||||
// do not run any of this logic when rewinding, as it inverts order of presses/releases.
|
||||
if (Time.Elapsed < 0)
|
||||
return;
|
||||
|
||||
// Make sure a hold was started
|
||||
if (HoldStartTime == null)
|
||||
return;
|
||||
|
||||
// do not run any of this logic when rewinding, as it inverts order of presses/releases.
|
||||
if (Time.Elapsed < 0)
|
||||
return;
|
||||
|
||||
Tail.UpdateResult();
|
||||
endHold();
|
||||
|
||||
@@ -350,13 +352,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
{
|
||||
// Note: base.LoadSamples() isn't called since the slider plays the tail's hitsounds for the time being.
|
||||
|
||||
if (HitObject.SampleControlPoint == null)
|
||||
{
|
||||
throw new InvalidOperationException($"{nameof(HitObject)}s must always have an attached {nameof(HitObject.SampleControlPoint)}."
|
||||
+ $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}.");
|
||||
}
|
||||
|
||||
slidingSample.Samples = HitObject.CreateSlidingSamples().Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast<ISampleInfo>().ToArray();
|
||||
slidingSample.Samples = HitObject.CreateSlidingSamples().Cast<ISampleInfo>().ToArray();
|
||||
}
|
||||
|
||||
public override void StopAllSamples()
|
||||
|
||||
+2
-2
@@ -10,7 +10,7 @@
|
||||
["Gameplay/soft-hitnormal"],
|
||||
["Gameplay/drum-hitnormal"]
|
||||
],
|
||||
"Samples": ["Gameplay/-hitnormal"]
|
||||
"Samples": ["Gameplay/normal-hitnormal"]
|
||||
}, {
|
||||
"StartTime": 1875.0,
|
||||
"EndTime": 2750.0,
|
||||
@@ -19,7 +19,7 @@
|
||||
["Gameplay/soft-hitnormal"],
|
||||
["Gameplay/drum-hitnormal"]
|
||||
],
|
||||
"Samples": ["Gameplay/-hitnormal"]
|
||||
"Samples": ["Gameplay/normal-hitnormal"]
|
||||
}]
|
||||
}, {
|
||||
"StartTime": 3750.0,
|
||||
|
||||
@@ -138,8 +138,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
|
||||
return slider1 is not null && mergedSlider.HeadCircle.Samples.SequenceEqual(slider1.HeadCircle.Samples)
|
||||
&& mergedSlider.TailCircle.Samples.SequenceEqual(slider1.TailCircle.Samples)
|
||||
&& mergedSlider.Samples.SequenceEqual(slider1.Samples)
|
||||
&& mergedSlider.SampleControlPoint.IsRedundant(slider1.SampleControlPoint);
|
||||
&& mergedSlider.Samples.SequenceEqual(slider1.Samples);
|
||||
});
|
||||
|
||||
AddAssert("slider end is at same completion for last slider", () =>
|
||||
|
||||
@@ -181,10 +181,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
if (slider is null) return;
|
||||
|
||||
slider.SampleControlPoint.SampleBank = "soft";
|
||||
slider.SampleControlPoint.SampleVolume = 70;
|
||||
sample = new HitSampleInfo("hitwhistle");
|
||||
slider.Samples.Add(sample);
|
||||
sample = new HitSampleInfo("hitwhistle", "soft", volume: 70);
|
||||
slider.Samples.Add(sample.With());
|
||||
});
|
||||
|
||||
AddStep("select added slider", () =>
|
||||
@@ -207,9 +205,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
AddAssert("sliders have hitsounds", hasHitsounds);
|
||||
|
||||
bool hasHitsounds() => sample is not null &&
|
||||
EditorBeatmap.HitObjects.All(o => o.SampleControlPoint.SampleBank == "soft" &&
|
||||
o.SampleControlPoint.SampleVolume == 70 &&
|
||||
o.Samples.Contains(sample));
|
||||
EditorBeatmap.HitObjects.All(o => o.Samples.Contains(sample));
|
||||
}
|
||||
|
||||
private bool sliderCreatedFor(Slider s, double startTime, double endTime, params (Vector2 pos, PathType? pathType)[] expectedControlPoints)
|
||||
|
||||
@@ -199,8 +199,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
Precision.AlmostEquals(circle.StartTime, time, 1)
|
||||
&& Precision.AlmostEquals(circle.Position, position, 0.01f)
|
||||
&& circle.NewCombo == startsNewCombo
|
||||
&& circle.Samples.SequenceEqual(slider.HeadCircle.Samples)
|
||||
&& circle.SampleControlPoint.IsRedundant(slider.SampleControlPoint);
|
||||
&& circle.Samples.SequenceEqual(slider.HeadCircle.Samples);
|
||||
}
|
||||
|
||||
private bool sliderRestored(Slider slider)
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
{
|
||||
public partial class TestSceneOsuModBubbles : OsuModTestScene
|
||||
{
|
||||
[Test]
|
||||
public void TestOsuModBubbles() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new OsuModBubbles(),
|
||||
Autoplay = true,
|
||||
PassCondition = () => true
|
||||
});
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
@@ -8,6 +8,7 @@ using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
@@ -18,6 +19,7 @@ using osu.Framework.Testing.Input;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Osu.Configuration;
|
||||
using osu.Game.Rulesets.Osu.Skinning;
|
||||
using osu.Game.Rulesets.Osu.UI.Cursor;
|
||||
using osu.Game.Screens.Play;
|
||||
@@ -40,6 +42,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
private Drawable background;
|
||||
|
||||
private readonly Bindable<bool> ripples = new Bindable<bool>();
|
||||
|
||||
public TestSceneGameplayCursor()
|
||||
{
|
||||
var ruleset = new OsuRuleset();
|
||||
@@ -57,6 +61,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
});
|
||||
});
|
||||
|
||||
AddToggleStep("ripples", v => ripples.Value = v);
|
||||
|
||||
AddSliderStep("circle size", 0f, 10f, 0f, val =>
|
||||
{
|
||||
config.SetValue(OsuSetting.AutoCursorSize, true);
|
||||
@@ -67,6 +73,13 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
AddStep("test cursor container", () => loadContent(false));
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
var rulesetConfig = (OsuRulesetConfigManager)RulesetConfigs.GetConfigFor(Ruleset.Value.CreateInstance()).AsNonNull();
|
||||
rulesetConfig.BindWith(OsuRulesetSetting.ShowCursorRipples, ripples);
|
||||
}
|
||||
|
||||
[TestCase(1, 1)]
|
||||
[TestCase(5, 1)]
|
||||
[TestCase(10, 1)]
|
||||
|
||||
@@ -439,7 +439,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
public TestSlider()
|
||||
{
|
||||
DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = 0.1f };
|
||||
SliderVelocity = 0.1f;
|
||||
|
||||
DefaultsApplied += _ =>
|
||||
{
|
||||
|
||||
@@ -15,6 +15,10 @@ using osuTK.Graphics;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps.Legacy;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
@@ -22,6 +26,7 @@ using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Configuration;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
@@ -30,6 +35,27 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
private int depthIndex;
|
||||
|
||||
private readonly BindableBool snakingIn = new BindableBool();
|
||||
private readonly BindableBool snakingOut = new BindableBool();
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddToggleStep("toggle snaking", v =>
|
||||
{
|
||||
snakingIn.Value = v;
|
||||
snakingOut.Value = v;
|
||||
});
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
var config = (OsuRulesetConfigManager)RulesetConfigs.GetConfigFor(Ruleset.Value.CreateInstance()).AsNonNull();
|
||||
config.BindWith(OsuRulesetSetting.SnakingInSliders, snakingIn);
|
||||
config.BindWith(OsuRulesetSetting.SnakingOutSliders, snakingOut);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestVariousSliders()
|
||||
{
|
||||
|
||||
@@ -7,7 +7,6 @@ using NUnit.Framework;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@@ -47,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
StartTime = time_slider_start,
|
||||
Position = new Vector2(0, 0),
|
||||
DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = velocity },
|
||||
SliderVelocity = velocity,
|
||||
Path = new SliderPath(PathType.Linear, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
|
||||
@@ -8,7 +8,6 @@ using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@@ -350,7 +349,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
StartTime = time_slider_start,
|
||||
Position = new Vector2(0, 0),
|
||||
DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = 0.1f },
|
||||
SliderVelocity = 0.1f,
|
||||
Path = new SliderPath(PathType.PerfectCurve, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
|
||||
@@ -74,7 +74,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
EndTime = Time.Current + delay + length,
|
||||
Samples = new List<HitSampleInfo>
|
||||
{
|
||||
new HitSampleInfo("hitnormal")
|
||||
new HitSampleInfo(HitSampleInfo.HIT_NORMAL)
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -399,7 +399,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
public TestSlider()
|
||||
{
|
||||
DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = 0.1f };
|
||||
SliderVelocity = 0.1f;
|
||||
|
||||
DefaultsApplied += _ =>
|
||||
{
|
||||
|
||||
@@ -30,6 +30,8 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
||||
{
|
||||
var positionData = original as IHasPosition;
|
||||
var comboData = original as IHasCombo;
|
||||
var sliderVelocityData = original as IHasSliderVelocity;
|
||||
var generateTicksData = original as IHasGenerateTicks;
|
||||
|
||||
switch (original)
|
||||
{
|
||||
@@ -47,7 +49,9 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
||||
LegacyLastTickOffset = (original as IHasLegacyLastTickOffset)?.LegacyLastTickOffset,
|
||||
// prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance.
|
||||
// this results in more (or less) ticks being generated in <v8 maps for the same time duration.
|
||||
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(original.StartTime).SliderVelocity : 1
|
||||
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(original.StartTime).SliderVelocity : 1,
|
||||
GenerateTicks = generateTicksData?.GenerateTicks ?? true,
|
||||
SliderVelocity = sliderVelocityData?.SliderVelocity ?? 1,
|
||||
}.Yield();
|
||||
|
||||
case IHasDuration endTimeData:
|
||||
|
||||
@@ -22,6 +22,7 @@ namespace osu.Game.Rulesets.Osu.Configuration
|
||||
SetDefault(OsuRulesetSetting.SnakingInSliders, true);
|
||||
SetDefault(OsuRulesetSetting.SnakingOutSliders, true);
|
||||
SetDefault(OsuRulesetSetting.ShowCursorTrail, true);
|
||||
SetDefault(OsuRulesetSetting.ShowCursorRipples, false);
|
||||
SetDefault(OsuRulesetSetting.PlayfieldBorderStyle, PlayfieldBorderStyle.None);
|
||||
}
|
||||
}
|
||||
@@ -31,6 +32,7 @@ namespace osu.Game.Rulesets.Osu.Configuration
|
||||
SnakingInSliders,
|
||||
SnakingOutSliders,
|
||||
ShowCursorTrail,
|
||||
ShowCursorRipples,
|
||||
PlayfieldBorderStyle,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
@@ -42,6 +41,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IDistanceSnapProvider snapProvider { get; set; }
|
||||
|
||||
protected override bool IsValidForPlacement => HitObject.Path.HasValidLength;
|
||||
|
||||
public SliderPlacementBlueprint()
|
||||
: base(new Slider())
|
||||
{
|
||||
@@ -83,11 +84,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
case SliderPlacementState.Initial:
|
||||
BeginPlacement();
|
||||
|
||||
var nearestDifficultyPoint = editorBeatmap.HitObjects
|
||||
.LastOrDefault(h => h is Slider && h.GetEndTime() < HitObject.StartTime)?
|
||||
.DifficultyControlPoint?.DeepClone() as DifficultyControlPoint;
|
||||
double? nearestSliderVelocity = (editorBeatmap.HitObjects
|
||||
.LastOrDefault(h => h is Slider && h.GetEndTime() < HitObject.StartTime) as Slider)?.SliderVelocity;
|
||||
|
||||
HitObject.DifficultyControlPoint = nearestDifficultyPoint ?? new DifficultyControlPoint();
|
||||
HitObject.SliderVelocity = nearestSliderVelocity ?? 1;
|
||||
HitObject.Position = ToLocalSpace(result.ScreenSpacePosition);
|
||||
|
||||
// Replacing the DifficultyControlPoint above doesn't trigger any kind of invalidation.
|
||||
@@ -152,7 +152,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
private void endCurve()
|
||||
{
|
||||
updateSlider();
|
||||
EndPlacement(HitObject.Path.HasValidLength);
|
||||
EndPlacement(true);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
|
||||
@@ -14,7 +14,6 @@ using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@@ -311,17 +310,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
var splitControlPoints = controlPoints.Take(index + 1).ToList();
|
||||
controlPoints.RemoveRange(0, index);
|
||||
|
||||
// Turn the control points which were split off into a new slider.
|
||||
var samplePoint = (SampleControlPoint)HitObject.SampleControlPoint.DeepClone();
|
||||
var difficultyPoint = (DifficultyControlPoint)HitObject.DifficultyControlPoint.DeepClone();
|
||||
|
||||
var newSlider = new Slider
|
||||
{
|
||||
StartTime = HitObject.StartTime,
|
||||
Position = HitObject.Position + splitControlPoints[0].Position,
|
||||
NewCombo = HitObject.NewCombo,
|
||||
SampleControlPoint = samplePoint,
|
||||
DifficultyControlPoint = difficultyPoint,
|
||||
LegacyLastTickOffset = HitObject.LegacyLastTickOffset,
|
||||
Samples = HitObject.Samples.Select(s => s.With()).ToList(),
|
||||
RepeatCount = HitObject.RepeatCount,
|
||||
@@ -378,15 +371,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
Vector2 position = HitObject.Position + HitObject.Path.PositionAt(pathPosition);
|
||||
|
||||
var samplePoint = (SampleControlPoint)HitObject.SampleControlPoint.DeepClone();
|
||||
samplePoint.Time = time;
|
||||
|
||||
editorBeatmap.Add(new HitCircle
|
||||
{
|
||||
StartTime = time,
|
||||
Position = position,
|
||||
NewCombo = i == 0 && HitObject.NewCombo,
|
||||
SampleControlPoint = samplePoint,
|
||||
Samples = HitObject.HeadCircle.Samples.Select(s => s.With()).ToList()
|
||||
});
|
||||
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Events;
|
||||
@@ -24,9 +21,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
|
||||
|
||||
private bool isPlacingEnd;
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
[CanBeNull]
|
||||
private IBeatSnapProvider beatSnapProvider { get; set; }
|
||||
[Resolved]
|
||||
private IBeatSnapProvider? beatSnapProvider { get; set; }
|
||||
|
||||
public SpinnerPlacementBlueprint()
|
||||
: base(new Spinner { Position = OsuPlayfield.BASE_SIZE / 2 })
|
||||
|
||||
@@ -13,8 +13,8 @@ using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Input.Bindings;
|
||||
@@ -62,7 +62,12 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
private void load()
|
||||
{
|
||||
// Give a bit of breathing room around the playfield content.
|
||||
PlayfieldContentContainer.Padding = new MarginPadding(10);
|
||||
PlayfieldContentContainer.Padding = new MarginPadding
|
||||
{
|
||||
Vertical = 10,
|
||||
Left = TOOLBOX_CONTRACTED_SIZE_LEFT + 10,
|
||||
Right = TOOLBOX_CONTRACTED_SIZE_RIGHT + 10,
|
||||
};
|
||||
|
||||
LayerBelowRuleset.AddRange(new Drawable[]
|
||||
{
|
||||
|
||||
@@ -362,7 +362,6 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
StartTime = firstHitObject.StartTime,
|
||||
Position = firstHitObject.Position,
|
||||
NewCombo = firstHitObject.NewCombo,
|
||||
SampleControlPoint = firstHitObject.SampleControlPoint,
|
||||
Samples = firstHitObject.Samples,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// 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.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
@@ -10,6 +11,8 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModBarrelRoll : ModBarrelRoll<OsuHitObject>, IApplicableToDrawableHitObject
|
||||
{
|
||||
public override Type[] IncompatibleMods => new[] { typeof(OsuModBubbles) };
|
||||
|
||||
public void ApplyToDrawableHitObject(DrawableHitObject d)
|
||||
{
|
||||
d.OnUpdate += _ =>
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
// 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.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Scoring;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public partial class OsuModBubbles : Mod, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToDrawableHitObject, IApplicableToScoreProcessor
|
||||
{
|
||||
public override string Name => "Bubbles";
|
||||
|
||||
public override string Acronym => "BU";
|
||||
|
||||
public override LocalisableString Description => "Don't let their popping distract you!";
|
||||
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
public override ModType Type => ModType.Fun;
|
||||
|
||||
// Compatibility with these seems potentially feasible in the future, blocked for now because they don't work as one would expect
|
||||
public override Type[] IncompatibleMods => new[] { typeof(OsuModBarrelRoll), typeof(OsuModMagnetised), typeof(OsuModRepel) };
|
||||
|
||||
private PlayfieldAdjustmentContainer bubbleContainer = null!;
|
||||
|
||||
private readonly Bindable<int> currentCombo = new BindableInt();
|
||||
|
||||
private float maxSize;
|
||||
private float bubbleSize;
|
||||
private double bubbleFade;
|
||||
|
||||
private readonly DrawablePool<BubbleDrawable> bubblePool = new DrawablePool<BubbleDrawable>(100);
|
||||
|
||||
public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank;
|
||||
|
||||
public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
|
||||
{
|
||||
currentCombo.BindTo(scoreProcessor.Combo);
|
||||
currentCombo.BindValueChanged(combo =>
|
||||
maxSize = Math.Min(1.75f, (float)(1.25 + 0.005 * combo.NewValue)), true);
|
||||
}
|
||||
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
|
||||
{
|
||||
// Multiplying by 2 results in an initial size that is too large, hence 1.90 has been chosen
|
||||
// Also avoids the HitObject bleeding around the edges of the bubble drawable at minimum size
|
||||
bubbleSize = (float)(drawableRuleset.Beatmap.HitObjects.OfType<HitCircle>().First().Radius * 1.90f);
|
||||
bubbleFade = drawableRuleset.Beatmap.HitObjects.OfType<HitCircle>().First().TimePreempt * 2;
|
||||
|
||||
// We want to hide the judgements since they are obscured by the BubbleDrawable (due to layering)
|
||||
drawableRuleset.Playfield.DisplayJudgements.Value = false;
|
||||
|
||||
bubbleContainer = drawableRuleset.CreatePlayfieldAdjustmentContainer();
|
||||
|
||||
drawableRuleset.Overlays.Add(bubbleContainer);
|
||||
}
|
||||
|
||||
public void ApplyToDrawableHitObject(DrawableHitObject drawableObject)
|
||||
{
|
||||
drawableObject.OnNewResult += (drawable, _) =>
|
||||
{
|
||||
if (drawable is not DrawableOsuHitObject drawableOsuHitObject) return;
|
||||
|
||||
switch (drawableOsuHitObject.HitObject)
|
||||
{
|
||||
case Slider:
|
||||
case SpinnerTick:
|
||||
break;
|
||||
|
||||
default:
|
||||
addBubble();
|
||||
break;
|
||||
}
|
||||
|
||||
void addBubble()
|
||||
{
|
||||
BubbleDrawable bubble = bubblePool.Get();
|
||||
|
||||
bubble.DrawableOsuHitObject = drawableOsuHitObject;
|
||||
bubble.InitialSize = new Vector2(bubbleSize);
|
||||
bubble.FadeTime = bubbleFade;
|
||||
bubble.MaxSize = maxSize;
|
||||
|
||||
bubbleContainer.Add(bubble);
|
||||
}
|
||||
};
|
||||
|
||||
drawableObject.OnRevertResult += (drawable, _) =>
|
||||
{
|
||||
if (drawable.HitObject is SpinnerTick or Slider) return;
|
||||
|
||||
BubbleDrawable? lastBubble = bubbleContainer.OfType<BubbleDrawable>().LastOrDefault();
|
||||
|
||||
lastBubble?.ClearTransforms();
|
||||
lastBubble?.Expire(true);
|
||||
};
|
||||
}
|
||||
|
||||
#region Pooled Bubble drawable
|
||||
|
||||
private partial class BubbleDrawable : PoolableDrawable
|
||||
{
|
||||
public DrawableOsuHitObject? DrawableOsuHitObject { get; set; }
|
||||
|
||||
public Vector2 InitialSize { get; set; }
|
||||
|
||||
public float MaxSize { get; set; }
|
||||
|
||||
public double FadeTime { get; set; }
|
||||
|
||||
private readonly Box colourBox;
|
||||
private readonly CircularContainer content;
|
||||
|
||||
public BubbleDrawable()
|
||||
{
|
||||
Origin = Anchor.Centre;
|
||||
InternalChild = content = new CircularContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
MaskingSmoothness = 2,
|
||||
BorderThickness = 0,
|
||||
BorderColour = Colour4.White,
|
||||
Masking = true,
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Shadow,
|
||||
Radius = 3,
|
||||
Colour = Colour4.Black.Opacity(0.05f),
|
||||
},
|
||||
Child = colourBox = new Box { RelativeSizeAxes = Axes.Both, }
|
||||
};
|
||||
}
|
||||
|
||||
protected override void PrepareForUse()
|
||||
{
|
||||
Debug.Assert(DrawableOsuHitObject.IsNotNull());
|
||||
|
||||
Colour = DrawableOsuHitObject.IsHit ? Colour4.White : Colour4.Black;
|
||||
Scale = new Vector2(1);
|
||||
Position = getPosition(DrawableOsuHitObject);
|
||||
Size = InitialSize;
|
||||
|
||||
//We want to fade to a darker colour to avoid colours such as white hiding the "ripple" effect.
|
||||
ColourInfo colourDarker = DrawableOsuHitObject.AccentColour.Value.Darken(0.1f);
|
||||
|
||||
// The absolute length of the bubble's animation, can be used in fractions for animations of partial length
|
||||
double duration = 1700 + Math.Pow(FadeTime, 1.07f);
|
||||
|
||||
// Main bubble scaling based on combo
|
||||
this.FadeTo(1)
|
||||
.ScaleTo(MaxSize, duration * 0.8f)
|
||||
.Then()
|
||||
// Pop at the end of the bubbles life time
|
||||
.ScaleTo(MaxSize * 1.5f, duration * 0.2f, Easing.OutQuint)
|
||||
.FadeOut(duration * 0.2f, Easing.OutCirc).Expire();
|
||||
|
||||
if (!DrawableOsuHitObject.IsHit) return;
|
||||
|
||||
content.BorderThickness = InitialSize.X / 3.5f;
|
||||
content.BorderColour = Colour4.White;
|
||||
|
||||
colourBox.FadeColour(colourDarker);
|
||||
|
||||
content.TransformTo(nameof(BorderColour), colourDarker, duration * 0.3f, Easing.OutQuint);
|
||||
// Ripple effect utilises the border to reduce drawable count
|
||||
content.TransformTo(nameof(BorderThickness), 2f, duration * 0.3f, Easing.OutQuint)
|
||||
.Then()
|
||||
// Avoids transparency overlap issues during the bubble "pop"
|
||||
.TransformTo(nameof(BorderThickness), 0f);
|
||||
}
|
||||
|
||||
private Vector2 getPosition(DrawableOsuHitObject drawableObject)
|
||||
{
|
||||
switch (drawableObject)
|
||||
{
|
||||
// SliderHeads are derived from HitCircles,
|
||||
// so we must handle them before to avoid them using the wrong positioning logic
|
||||
case DrawableSliderHead:
|
||||
return drawableObject.HitObject.Position;
|
||||
|
||||
// Using hitobject position will cause issues with HitCircle placement due to stack leniency.
|
||||
case DrawableHitCircle:
|
||||
return drawableObject.Position;
|
||||
|
||||
default:
|
||||
return drawableObject.HitObject.Position;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public override ModType Type => ModType.Fun;
|
||||
public override LocalisableString Description => "No need to chase the circles – your cursor is a magnet!";
|
||||
public override double ScoreMultiplier => 0.5;
|
||||
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax), typeof(OsuModRepel) };
|
||||
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax), typeof(OsuModRepel), typeof(OsuModBubbles) };
|
||||
|
||||
[SettingSource("Attraction strength", "How strong the pull is.", 0)]
|
||||
public BindableFloat AttractionStrength { get; } = new BindableFloat(0.5f)
|
||||
|
||||
@@ -9,7 +9,6 @@ using osu.Framework.Localisation;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||
@@ -28,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModTargetPractice)).ToArray();
|
||||
|
||||
[SettingSource("Angle sharpness", "How sharp angles should be", SettingControlType = typeof(SettingsSlider<float>))]
|
||||
[SettingSource("Angle sharpness", "How sharp angles should be")]
|
||||
public BindableFloat AngleSharpness { get; } = new BindableFloat(7)
|
||||
{
|
||||
MinValue = 1,
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public override ModType Type => ModType.Fun;
|
||||
public override LocalisableString Description => "Hit objects run away!";
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModMagnetised) };
|
||||
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModMagnetised), typeof(OsuModBubbles) };
|
||||
|
||||
[SettingSource("Repulsion strength", "How strong the repulsion is.", 0)]
|
||||
public BindableFloat RepulsionStrength { get; } = new BindableFloat(0.5f)
|
||||
|
||||
@@ -98,6 +98,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
ComboOffset = original.ComboOffset;
|
||||
LegacyLastTickOffset = original.LegacyLastTickOffset;
|
||||
TickDistanceMultiplier = original.TickDistanceMultiplier;
|
||||
SliderVelocity = original.SliderVelocity;
|
||||
}
|
||||
|
||||
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
||||
|
||||
@@ -133,14 +133,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
// Note: base.LoadSamples() isn't called since the slider plays the tail's hitsounds for the time being.
|
||||
|
||||
if (HitObject.SampleControlPoint == null)
|
||||
{
|
||||
throw new InvalidOperationException($"{nameof(HitObject)}s must always have an attached {nameof(HitObject.SampleControlPoint)}."
|
||||
+ $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}.");
|
||||
}
|
||||
|
||||
Samples.Samples = HitObject.TailSamples.Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast<ISampleInfo>().ToArray();
|
||||
slidingSample.Samples = HitObject.CreateSlidingSamples().Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast<ISampleInfo>().ToArray();
|
||||
Samples.Samples = HitObject.TailSamples.Cast<ISampleInfo>().ToArray();
|
||||
slidingSample.Samples = HitObject.CreateSlidingSamples().Cast<ISampleInfo>().ToArray();
|
||||
}
|
||||
|
||||
public override void StopAllSamples()
|
||||
|
||||
@@ -87,12 +87,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
protected override void UpdateInitialTransforms()
|
||||
{
|
||||
// When snaking in is enabled, the first end circle needs to be delayed until the snaking completes.
|
||||
bool delayFadeIn = DrawableSlider.SliderBody?.SnakingIn.Value == true && HitObject.RepeatIndex == 0;
|
||||
|
||||
animDuration = Math.Min(300, HitObject.SpanDuration);
|
||||
|
||||
this.Animate(
|
||||
d => d.FadeIn(animDuration),
|
||||
d => d.ScaleTo(0.5f).ScaleTo(1f, animDuration * 2, Easing.OutElasticHalf)
|
||||
);
|
||||
this
|
||||
.FadeOut()
|
||||
.Delay(delayFadeIn ? (Slider?.TimePreempt ?? 0) / 3 : 0)
|
||||
.FadeIn(HitObject.RepeatIndex == 0 ? HitObject.TimeFadeIn : animDuration);
|
||||
}
|
||||
|
||||
protected override void UpdateHitStateTransforms(ArmedState state)
|
||||
|
||||
@@ -91,7 +91,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
base.UpdateInitialTransforms();
|
||||
|
||||
CirclePiece.FadeInFromZero(HitObject.TimeFadeIn);
|
||||
// When snaking in is enabled, the first end circle needs to be delayed until the snaking completes.
|
||||
bool delayFadeIn = DrawableSlider.SliderBody?.SnakingIn.Value == true && HitObject.RepeatIndex == 0;
|
||||
|
||||
CirclePiece
|
||||
.FadeOut()
|
||||
.Delay(delayFadeIn ? (Slider?.TimePreempt ?? 0) / 3 : 0)
|
||||
.FadeIn(HitObject.TimeFadeIn);
|
||||
}
|
||||
|
||||
protected override void UpdateHitStateTransforms(ArmedState state)
|
||||
|
||||
@@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
base.LoadSamples();
|
||||
|
||||
spinningSample.Samples = HitObject.CreateSpinningSamples().Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast<ISampleInfo>().ToArray();
|
||||
spinningSample.Samples = HitObject.CreateSpinningSamples().Cast<ISampleInfo>().ToArray();
|
||||
spinningSample.Frequency.Value = spinning_sample_initial_frequency;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,18 +10,18 @@ using osu.Game.Rulesets.Objects;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Caching;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Beatmaps.Formats;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects
|
||||
{
|
||||
public class Slider : OsuHitObject, IHasPathWithRepeats
|
||||
public class Slider : OsuHitObject, IHasPathWithRepeats, IHasSliderVelocity, IHasGenerateTicks
|
||||
{
|
||||
public double EndTime => StartTime + this.SpanCount() * Path.Distance / Velocity;
|
||||
|
||||
@@ -134,6 +134,21 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
/// </summary>
|
||||
public bool OnlyJudgeNestedObjects = true;
|
||||
|
||||
public BindableNumber<double> SliderVelocityBindable { get; } = new BindableDouble(1)
|
||||
{
|
||||
Precision = 0.01,
|
||||
MinValue = 0.1,
|
||||
MaxValue = 10
|
||||
};
|
||||
|
||||
public double SliderVelocity
|
||||
{
|
||||
get => SliderVelocityBindable.Value;
|
||||
set => SliderVelocityBindable.Value = value;
|
||||
}
|
||||
|
||||
public bool GenerateTicks { get; set; } = true;
|
||||
|
||||
[JsonIgnore]
|
||||
public SliderHeadCircle HeadCircle { get; protected set; }
|
||||
|
||||
@@ -151,15 +166,11 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
|
||||
|
||||
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
|
||||
#pragma warning disable 618
|
||||
var legacyDifficultyPoint = DifficultyControlPoint as LegacyBeatmapDecoder.LegacyDifficultyControlPoint;
|
||||
#pragma warning restore 618
|
||||
|
||||
double scoringDistance = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity;
|
||||
bool generateTicks = legacyDifficultyPoint?.GenerateTicks ?? true;
|
||||
double scoringDistance = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier * SliderVelocity;
|
||||
|
||||
Velocity = scoringDistance / timingPoint.BeatLength;
|
||||
TickDistance = generateTicks ? (scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier) : double.PositiveInfinity;
|
||||
TickDistance = GenerateTicks ? (scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier) : double.PositiveInfinity;
|
||||
}
|
||||
|
||||
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
||||
|
||||
@@ -39,11 +39,8 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
}
|
||||
else
|
||||
{
|
||||
// taken from osu-stable
|
||||
const float first_end_circle_preempt_adjust = 2 / 3f;
|
||||
|
||||
// The first end circle should fade in with the slider.
|
||||
TimePreempt = (StartTime - slider.StartTime) + slider.TimePreempt * first_end_circle_preempt_adjust;
|
||||
TimePreempt += StartTime - slider.StartTime;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
|
||||
AddNested(i < SpinsRequired
|
||||
? new SpinnerTick { StartTime = startTime, SpinnerDuration = Duration }
|
||||
: new SpinnerBonusTick { StartTime = startTime, SpinnerDuration = Duration });
|
||||
: new SpinnerBonusTick { StartTime = startTime, SpinnerDuration = Duration, Samples = new[] { CreateHitSampleInfo("spinnerbonus") } });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
|
||||
return new[]
|
||||
{
|
||||
SampleControlPoint.ApplyTo(referenceSample).With("spinnerspin")
|
||||
referenceSample.With("spinnerspin")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
@@ -11,11 +10,6 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
{
|
||||
public class SpinnerBonusTick : SpinnerTick
|
||||
{
|
||||
public SpinnerBonusTick()
|
||||
{
|
||||
Samples.Add(new HitSampleInfo("spinnerbonus"));
|
||||
}
|
||||
|
||||
public override Judgement CreateJudgement() => new OsuSpinnerBonusTickJudgement();
|
||||
|
||||
public class OsuSpinnerBonusTickJudgement : OsuSpinnerTickJudgement
|
||||
|
||||
@@ -203,7 +203,8 @@ namespace osu.Game.Rulesets.Osu
|
||||
new OsuModNoScope(),
|
||||
new MultiMod(new OsuModMagnetised(), new OsuModRepel()),
|
||||
new ModAdaptiveSpeed(),
|
||||
new OsuModFreezeFrame()
|
||||
new OsuModFreezeFrame(),
|
||||
new OsuModBubbles()
|
||||
};
|
||||
|
||||
case ModType.System:
|
||||
|
||||
@@ -10,6 +10,7 @@ namespace osu.Game.Rulesets.Osu
|
||||
Cursor,
|
||||
CursorTrail,
|
||||
CursorParticles,
|
||||
CursorRipple,
|
||||
SliderScorePoint,
|
||||
ReverseArrow,
|
||||
HitCircleText,
|
||||
|
||||
@@ -98,7 +98,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
|
||||
using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
|
||||
{
|
||||
this.FadeOut(duration, Easing.OutQuint);
|
||||
// intentionally pile on an extra FadeOut to make it happen much faster
|
||||
this.FadeOut(duration / 4, Easing.OutQuint);
|
||||
icon.ScaleTo(defaultIconScale * icon_scale, duration, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
{
|
||||
@@ -18,6 +20,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
|
||||
private Drawable proxy = null!;
|
||||
|
||||
private Bindable<Color4> accentColour = null!;
|
||||
|
||||
private bool textureIsDefaultSkin;
|
||||
|
||||
private Drawable arrow = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ISkinSource skinSource)
|
||||
{
|
||||
@@ -26,7 +34,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
string lookupName = new OsuSkinComponentLookup(OsuSkinComponents.ReverseArrow).LookupName;
|
||||
|
||||
var skin = skinSource.FindProvider(s => s.GetTexture(lookupName) != null);
|
||||
InternalChild = skin?.GetAnimation(lookupName, true, true) ?? Empty();
|
||||
|
||||
InternalChild = arrow = (skin?.GetAnimation(lookupName, true, true) ?? Empty());
|
||||
textureIsDefaultSkin = skin is ISkinTransformer transformer && transformer.Skin is DefaultLegacySkin;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@@ -39,6 +49,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
{
|
||||
drawableHitObject.HitObjectApplied += onHitObjectApplied;
|
||||
onHitObjectApplied(drawableHitObject);
|
||||
|
||||
accentColour = drawableHitObject.AccentColour.GetBoundCopy();
|
||||
accentColour.BindValueChanged(c =>
|
||||
{
|
||||
arrow.Colour = textureIsDefaultSkin && c.NewValue.R + c.NewValue.G + c.NewValue.B > (600 / 255f) ? Color4.Black : Color4.White;
|
||||
}, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -100,6 +100,28 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
|
||||
return null;
|
||||
|
||||
case OsuSkinComponents.CursorRipple:
|
||||
if (GetTexture("cursor-ripple") != null)
|
||||
{
|
||||
var ripple = this.GetAnimation("cursor-ripple", false, false);
|
||||
|
||||
// In stable this element was scaled down to 50% and opacity 20%, but this makes the elements WAY too big and inflexible.
|
||||
// If anyone complains about these not being applied, this can be uncommented.
|
||||
//
|
||||
// But if no one complains I'd rather fix this in lazer. Wiki documentation doesn't mention size,
|
||||
// so we might be okay.
|
||||
//
|
||||
// if (ripple != null)
|
||||
// {
|
||||
// ripple.Scale = new Vector2(0.5f);
|
||||
// ripple.Alpha = 0.2f;
|
||||
// }
|
||||
|
||||
return ripple;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
case OsuSkinComponents.CursorParticles:
|
||||
if (GetTexture("star2") != null)
|
||||
return new LegacyCursorParticles();
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
// 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 osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Osu.Configuration;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Skinning.Default;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
{
|
||||
public partial class CursorRippleVisualiser : CompositeDrawable, IKeyBindingHandler<OsuAction>
|
||||
{
|
||||
private readonly Bindable<bool> showRipples = new Bindable<bool>(true);
|
||||
|
||||
private readonly DrawablePool<CursorRipple> ripplePool = new DrawablePool<CursorRipple>(20);
|
||||
|
||||
public CursorRippleVisualiser()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
public Vector2 CursorScale { get; set; } = Vector2.One;
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(OsuRulesetConfigManager? rulesetConfig)
|
||||
{
|
||||
rulesetConfig?.BindWith(OsuRulesetSetting.ShowCursorRipples, showRipples);
|
||||
}
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<OsuAction> e)
|
||||
{
|
||||
if (showRipples.Value)
|
||||
{
|
||||
AddInternal(ripplePool.Get(r =>
|
||||
{
|
||||
r.Position = e.MousePosition;
|
||||
r.Scale = CursorScale;
|
||||
}));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void OnReleased(KeyBindingReleaseEvent<OsuAction> e)
|
||||
{
|
||||
}
|
||||
|
||||
private partial class CursorRipple : PoolableDrawable
|
||||
{
|
||||
private Drawable ripple = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
InternalChild = ripple = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.CursorRipple), _ => new DefaultCursorRipple())
|
||||
{
|
||||
Blending = BlendingParameters.Additive,
|
||||
};
|
||||
}
|
||||
|
||||
protected override void PrepareForUse()
|
||||
{
|
||||
base.PrepareForUse();
|
||||
|
||||
ClearTransforms(true);
|
||||
|
||||
ripple.ScaleTo(0.1f)
|
||||
.ScaleTo(1, 700, Easing.Out);
|
||||
|
||||
this
|
||||
.FadeOutFromOne(700)
|
||||
.Expire(true);
|
||||
}
|
||||
}
|
||||
|
||||
public partial class DefaultCursorRipple : CompositeDrawable
|
||||
{
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new RingPiece(3)
|
||||
{
|
||||
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2),
|
||||
Alpha = 0.1f,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
private Bindable<float> userCursorScale;
|
||||
private Bindable<bool> autoCursorScale;
|
||||
|
||||
private readonly CursorRippleVisualiser rippleVisualiser;
|
||||
|
||||
public OsuCursorContainer()
|
||||
{
|
||||
InternalChild = fadeContainer = new Container
|
||||
@@ -48,6 +50,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
Children = new[]
|
||||
{
|
||||
cursorTrail = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.CursorTrail), _ => new DefaultCursorTrail(), confineMode: ConfineMode.NoScaling),
|
||||
rippleVisualiser = new CursorRippleVisualiser(),
|
||||
new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.CursorParticles), confineMode: ConfineMode.NoScaling),
|
||||
}
|
||||
};
|
||||
@@ -82,6 +85,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
var newScale = new Vector2(e.NewValue);
|
||||
|
||||
ActiveCursor.Scale = newScale;
|
||||
rippleVisualiser.CursorScale = newScale;
|
||||
cursorTrail.Scale = newScale;
|
||||
}, true);
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
|
||||
localCursorContainer?.Expire();
|
||||
localCursorContainer = null;
|
||||
GameplayCursor?.ActiveCursor?.Show();
|
||||
GameplayCursor?.ActiveCursor.Show();
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e) => true;
|
||||
|
||||
@@ -43,6 +43,11 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
LabelText = RulesetSettingsStrings.CursorTrail,
|
||||
Current = config.GetBindable<bool>(OsuRulesetSetting.ShowCursorTrail)
|
||||
},
|
||||
new SettingsCheckbox
|
||||
{
|
||||
LabelText = RulesetSettingsStrings.CursorRipples,
|
||||
Current = config.GetBindable<bool>(OsuRulesetSetting.ShowCursorRipples)
|
||||
},
|
||||
new SettingsEnumDropdown<PlayfieldBorderStyle>
|
||||
{
|
||||
LabelText = RulesetSettingsStrings.PlayfieldBorderStyle,
|
||||
|
||||
@@ -64,7 +64,9 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
|
||||
|
||||
foreach (HitObject hitObject in original.HitObjects)
|
||||
{
|
||||
double nextScrollSpeed = hitObject.DifficultyControlPoint.SliderVelocity;
|
||||
if (hitObject is not IHasSliderVelocity hasSliderVelocity) continue;
|
||||
|
||||
double nextScrollSpeed = hasSliderVelocity.SliderVelocity;
|
||||
EffectControlPoint currentEffectPoint = converted.ControlPointInfo.EffectPointAt(hitObject.StartTime);
|
||||
|
||||
if (!Precision.AlmostEquals(lastScrollSpeed, nextScrollSpeed, acceptableDifference: currentEffectPoint.ScrollSpeedBindable.Precision))
|
||||
@@ -131,7 +133,8 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
|
||||
StartTime = obj.StartTime,
|
||||
Samples = obj.Samples,
|
||||
Duration = taikoDuration,
|
||||
TickRate = beatmap.Difficulty.SliderTickRate == 3 ? 3 : 4
|
||||
TickRate = beatmap.Difficulty.SliderTickRate == 3 ? 3 : 4,
|
||||
SliderVelocity = obj is IHasSliderVelocity velocityData ? velocityData.SliderVelocity : 1
|
||||
};
|
||||
}
|
||||
|
||||
@@ -177,15 +180,14 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
|
||||
double distance = distanceData.Distance * spans * LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER;
|
||||
|
||||
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(obj.StartTime);
|
||||
DifficultyControlPoint difficultyPoint = obj.DifficultyControlPoint;
|
||||
|
||||
double beatLength;
|
||||
#pragma warning disable 618
|
||||
if (difficultyPoint is LegacyBeatmapDecoder.LegacyDifficultyControlPoint legacyDifficultyPoint)
|
||||
#pragma warning restore 618
|
||||
beatLength = timingPoint.BeatLength * legacyDifficultyPoint.BpmMultiplier;
|
||||
if (obj.LegacyBpmMultiplier.HasValue)
|
||||
beatLength = timingPoint.BeatLength * obj.LegacyBpmMultiplier.Value;
|
||||
else if (obj is IHasSliderVelocity hasSliderVelocity)
|
||||
beatLength = timingPoint.BeatLength / hasSliderVelocity.SliderVelocity;
|
||||
else
|
||||
beatLength = timingPoint.BeatLength / difficultyPoint.SliderVelocity;
|
||||
beatLength = timingPoint.BeatLength;
|
||||
|
||||
double sliderScoringPointDistance = osu_base_scoring_distance * beatmap.Difficulty.SliderMultiplier / beatmap.Difficulty.SliderTickRate;
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
|
||||
|
||||
private readonly IHasDuration spanPlacementObject;
|
||||
|
||||
protected override bool IsValidForPlacement => spanPlacementObject.Duration > 0;
|
||||
|
||||
public TaikoSpanPlacementBlueprint(HitObject hitObject)
|
||||
: base(hitObject)
|
||||
{
|
||||
@@ -73,7 +75,7 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
|
||||
return;
|
||||
|
||||
base.OnMouseUp(e);
|
||||
EndPlacement(spanPlacementObject.Duration > 0);
|
||||
EndPlacement(true);
|
||||
}
|
||||
|
||||
public override void UpdateTimeAndPosition(SnapResult result)
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<TaikoHitObject> drawableRuleset)
|
||||
{
|
||||
var drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset;
|
||||
drawableTaikoRuleset.LockPlayfieldMaxAspect.Value = false;
|
||||
drawableTaikoRuleset.LockPlayfieldAspectRange.Value = false;
|
||||
|
||||
var playfield = (TaikoPlayfield)drawableRuleset.Playfield;
|
||||
playfield.ClassicHitTargetPosition.Value = true;
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using System.Threading;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Beatmaps.Formats;
|
||||
@@ -15,7 +18,7 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Objects
|
||||
{
|
||||
public class DrumRoll : TaikoStrongableHitObject, IHasPath
|
||||
public class DrumRoll : TaikoStrongableHitObject, IHasPath, IHasSliderVelocity
|
||||
{
|
||||
/// <summary>
|
||||
/// Drum roll distance that results in a duration of 1 speed-adjusted beat length.
|
||||
@@ -35,6 +38,19 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
||||
/// </summary>
|
||||
public double Velocity { get; private set; }
|
||||
|
||||
public BindableNumber<double> SliderVelocityBindable { get; } = new BindableDouble(1)
|
||||
{
|
||||
Precision = 0.01,
|
||||
MinValue = 0.1,
|
||||
MaxValue = 10
|
||||
};
|
||||
|
||||
public double SliderVelocity
|
||||
{
|
||||
get => SliderVelocityBindable.Value;
|
||||
set => SliderVelocityBindable.Value = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Numer of ticks per beat length.
|
||||
/// </summary>
|
||||
@@ -52,7 +68,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
||||
|
||||
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
|
||||
|
||||
double scoringDistance = base_distance * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity;
|
||||
double scoringDistance = base_distance * difficulty.SliderMultiplier * SliderVelocity;
|
||||
Velocity = scoringDistance / timingPoint.BeatLength;
|
||||
|
||||
tickSpacing = timingPoint.BeatLength / TickRate;
|
||||
@@ -81,7 +97,8 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
||||
FirstTick = first,
|
||||
TickSpacing = tickSpacing,
|
||||
StartTime = t,
|
||||
IsStrong = IsStrong
|
||||
IsStrong = IsStrong,
|
||||
Samples = Samples.Where(s => s.Name == HitSampleInfo.HIT_FINISH).ToList()
|
||||
});
|
||||
|
||||
first = false;
|
||||
|
||||
@@ -63,7 +63,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
||||
if (isRimType != rimSamples.Any())
|
||||
{
|
||||
if (isRimType)
|
||||
Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_CLAP));
|
||||
Samples.Add(CreateHitSampleInfo(HitSampleInfo.HIT_CLAP));
|
||||
else
|
||||
{
|
||||
foreach (var sample in rimSamples)
|
||||
|
||||
@@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
||||
if (IsStrongBindable.Value != strongSamples.Any())
|
||||
{
|
||||
if (IsStrongBindable.Value)
|
||||
Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_FINISH));
|
||||
Samples.Add(CreateHitSampleInfo(HitSampleInfo.HIT_FINISH));
|
||||
else
|
||||
{
|
||||
foreach (var sample in strongSamples)
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
{
|
||||
public new BindableDouble TimeRange => base.TimeRange;
|
||||
|
||||
public readonly BindableBool LockPlayfieldMaxAspect = new BindableBool(true);
|
||||
public readonly BindableBool LockPlayfieldAspectRange = new BindableBool(true);
|
||||
|
||||
public new TaikoInputManager KeyBindingInputManager => (TaikoInputManager)base.KeyBindingInputManager;
|
||||
|
||||
@@ -69,7 +69,9 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
const float scroll_rate = 10;
|
||||
|
||||
// Since the time range will depend on a positional value, it is referenced to the x480 pixel space.
|
||||
float ratio = DrawHeight / 480;
|
||||
// Width is used because it defines how many notes fit on the playfield.
|
||||
// We clamp the ratio to the maximum aspect ratio to keep scroll speed consistent on widths lower than the default.
|
||||
float ratio = Math.Max(DrawSize.X / 768f, TaikoPlayfieldAdjustmentContainer.MAXIMUM_ASPECT);
|
||||
|
||||
TimeRange.Value = (Playfield.HitObjectContainer.DrawWidth / ratio) * scroll_rate;
|
||||
}
|
||||
@@ -92,7 +94,7 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
|
||||
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new TaikoPlayfieldAdjustmentContainer
|
||||
{
|
||||
LockPlayfieldMaxAspect = { BindTarget = LockPlayfieldMaxAspect }
|
||||
LockPlayfieldAspectRange = { BindTarget = LockPlayfieldAspectRange }
|
||||
};
|
||||
|
||||
protected override PassThroughInputManager CreateInputManager() => new TaikoInputManager(Ruleset.RulesetInfo);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Rulesets.UI;
|
||||
@@ -17,12 +18,12 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
|
||||
public void Play(HitType hitType)
|
||||
{
|
||||
var hitObject = GetMostValidObject();
|
||||
var hitSample = GetMostValidObject()?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL);
|
||||
|
||||
if (hitObject == null)
|
||||
if (hitSample == null)
|
||||
return;
|
||||
|
||||
PlaySamples(new ISampleInfo[] { hitObject.SampleControlPoint.GetSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL) });
|
||||
PlaySamples(new ISampleInfo[] { new HitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL, hitSample.Bank, volume: hitSample.Volume) });
|
||||
}
|
||||
|
||||
public override void Play() => throw new InvalidOperationException(@"Use override with HitType parameter instead");
|
||||
|
||||
@@ -11,9 +11,11 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
public partial class TaikoPlayfieldAdjustmentContainer : PlayfieldAdjustmentContainer
|
||||
{
|
||||
private const float default_relative_height = TaikoPlayfield.DEFAULT_HEIGHT / 768;
|
||||
private const float default_aspect = 16f / 9f;
|
||||
|
||||
public readonly IBindable<bool> LockPlayfieldMaxAspect = new BindableBool(true);
|
||||
public const float MAXIMUM_ASPECT = 16f / 9f;
|
||||
public const float MINIMUM_ASPECT = 5f / 4f;
|
||||
|
||||
public readonly IBindable<bool> LockPlayfieldAspectRange = new BindableBool(true);
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
@@ -26,12 +28,22 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
//
|
||||
// As a middle-ground, the aspect ratio can still be adjusted in the downwards direction but has a maximum limit.
|
||||
// This is still a bit weird, because readability changes with window size, but it is what it is.
|
||||
if (LockPlayfieldMaxAspect.Value && Parent.ChildSize.X / Parent.ChildSize.Y > default_aspect)
|
||||
height *= Math.Clamp(Parent.ChildSize.X / Parent.ChildSize.Y, 0.4f, 4) / default_aspect;
|
||||
if (LockPlayfieldAspectRange.Value)
|
||||
{
|
||||
float currentAspect = Parent.ChildSize.X / Parent.ChildSize.Y;
|
||||
|
||||
if (currentAspect > MAXIMUM_ASPECT)
|
||||
height *= currentAspect / MAXIMUM_ASPECT;
|
||||
else if (currentAspect < MINIMUM_ASPECT)
|
||||
height *= currentAspect / MINIMUM_ASPECT;
|
||||
}
|
||||
|
||||
// Limit the maximum relative height of the playfield to one-third of available area to avoid it masking out on extreme resolutions.
|
||||
height = Math.Min(height, 1f / 3f);
|
||||
Height = height;
|
||||
|
||||
// Position the taiko playfield exactly one playfield from the top of the screen.
|
||||
// Position the taiko playfield exactly one playfield from the top of the screen, if there is enough space for it.
|
||||
// Note that the relative height cannot exceed one-third - if that limit is hit, the playfield will be exactly centered.
|
||||
RelativePositionAxes = Axes.Y;
|
||||
Y = height;
|
||||
}
|
||||
|
||||
@@ -510,7 +510,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
Assert.AreEqual("Gameplay/soft-hitnormal8", getTestableSampleInfo(hitObjects[4]).LookupNames.First());
|
||||
}
|
||||
|
||||
static HitSampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.SampleControlPoint.ApplyTo(hitObject.Samples[0]);
|
||||
static HitSampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.Samples[0];
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -528,7 +528,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
Assert.AreEqual("Gameplay/normal-hitnormal3", getTestableSampleInfo(hitObjects[2]).LookupNames.First());
|
||||
}
|
||||
|
||||
static HitSampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.SampleControlPoint.ApplyTo(hitObject.Samples[0]);
|
||||
static HitSampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.Samples[0];
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -548,7 +548,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
Assert.AreEqual(70, getTestableSampleInfo(hitObjects[3]).Volume);
|
||||
}
|
||||
|
||||
static HitSampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.SampleControlPoint.ApplyTo(hitObject.Samples[0]);
|
||||
static HitSampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.Samples[0];
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
+47
-31
@@ -1,19 +1,26 @@
|
||||
// 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.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using Realms;
|
||||
|
||||
namespace osu.Game.Tests.Database
|
||||
{
|
||||
[TestFixture]
|
||||
public class LegacyExporterTest
|
||||
public class LegacyModelExporterTest
|
||||
{
|
||||
private TestLegacyExporter legacyExporter = null!;
|
||||
private TestLegacyModelExporter legacyExporter = null!;
|
||||
private TemporaryNativeStorage storage = null!;
|
||||
|
||||
private const string short_filename = "normal file name";
|
||||
@@ -25,15 +32,15 @@ namespace osu.Game.Tests.Database
|
||||
public void SetUp()
|
||||
{
|
||||
storage = new TemporaryNativeStorage("export-storage");
|
||||
legacyExporter = new TestLegacyExporter(storage);
|
||||
legacyExporter = new TestLegacyModelExporter(storage);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ExportFileWithNormalNameTest()
|
||||
{
|
||||
var item = new TestPathInfo(short_filename);
|
||||
var item = new TestModel(short_filename);
|
||||
|
||||
Assert.That(item.Filename.Length, Is.LessThan(TestLegacyExporter.MAX_FILENAME_LENGTH));
|
||||
Assert.That(item.Filename.Length, Is.LessThan(TestLegacyModelExporter.MAX_FILENAME_LENGTH));
|
||||
|
||||
exportItemAndAssert(item, short_filename);
|
||||
}
|
||||
@@ -41,9 +48,9 @@ namespace osu.Game.Tests.Database
|
||||
[Test]
|
||||
public void ExportFileWithNormalNameMultipleTimesTest()
|
||||
{
|
||||
var item = new TestPathInfo(short_filename);
|
||||
var item = new TestModel(short_filename);
|
||||
|
||||
Assert.That(item.Filename.Length, Is.LessThan(TestLegacyExporter.MAX_FILENAME_LENGTH));
|
||||
Assert.That(item.Filename.Length, Is.LessThan(TestLegacyModelExporter.MAX_FILENAME_LENGTH));
|
||||
|
||||
//Export multiple times
|
||||
for (int i = 0; i < 100; i++)
|
||||
@@ -56,24 +63,24 @@ namespace osu.Game.Tests.Database
|
||||
[Test]
|
||||
public void ExportFileWithSuperLongNameTest()
|
||||
{
|
||||
int expectedLength = TestLegacyExporter.MAX_FILENAME_LENGTH - (legacyExporter.GetExtension().Length);
|
||||
int expectedLength = TestLegacyModelExporter.MAX_FILENAME_LENGTH - (legacyExporter.GetExtension().Length);
|
||||
string expectedName = long_filename.Remove(expectedLength);
|
||||
|
||||
var item = new TestPathInfo(long_filename);
|
||||
var item = new TestModel(long_filename);
|
||||
|
||||
Assert.That(item.Filename.Length, Is.GreaterThan(TestLegacyExporter.MAX_FILENAME_LENGTH));
|
||||
Assert.That(item.Filename.Length, Is.GreaterThan(TestLegacyModelExporter.MAX_FILENAME_LENGTH));
|
||||
exportItemAndAssert(item, expectedName);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ExportFileWithSuperLongNameMultipleTimesTest()
|
||||
{
|
||||
int expectedLength = TestLegacyExporter.MAX_FILENAME_LENGTH - (legacyExporter.GetExtension().Length);
|
||||
int expectedLength = TestLegacyModelExporter.MAX_FILENAME_LENGTH - (legacyExporter.GetExtension().Length);
|
||||
string expectedName = long_filename.Remove(expectedLength);
|
||||
|
||||
var item = new TestPathInfo(long_filename);
|
||||
var item = new TestModel(long_filename);
|
||||
|
||||
Assert.That(item.Filename.Length, Is.GreaterThan(TestLegacyExporter.MAX_FILENAME_LENGTH));
|
||||
Assert.That(item.Filename.Length, Is.GreaterThan(TestLegacyModelExporter.MAX_FILENAME_LENGTH));
|
||||
|
||||
//Export multiple times
|
||||
for (int i = 0; i < 100; i++)
|
||||
@@ -83,9 +90,12 @@ namespace osu.Game.Tests.Database
|
||||
}
|
||||
}
|
||||
|
||||
private void exportItemAndAssert(IHasNamedFiles item, string expectedName)
|
||||
private void exportItemAndAssert(TestModel item, string expectedName)
|
||||
{
|
||||
Assert.DoesNotThrow(() => legacyExporter.Export(item));
|
||||
Assert.DoesNotThrow(() =>
|
||||
{
|
||||
Task.Run(() => legacyExporter.ExportAsync(new RealmLiveUnmanaged<TestModel>(item))).WaitSafely();
|
||||
});
|
||||
Assert.That(storage.Exists($"exports/{expectedName}{legacyExporter.GetExtension()}"), Is.True);
|
||||
}
|
||||
|
||||
@@ -96,30 +106,36 @@ namespace osu.Game.Tests.Database
|
||||
storage.Dispose();
|
||||
}
|
||||
|
||||
private class TestPathInfo : IHasNamedFiles
|
||||
private class TestLegacyModelExporter : LegacyExporter<TestModel>
|
||||
{
|
||||
public string Filename { get; }
|
||||
|
||||
public IEnumerable<INamedFileUsage> Files { get; } = new List<INamedFileUsage>();
|
||||
|
||||
public TestPathInfo(string filename)
|
||||
{
|
||||
Filename = filename;
|
||||
}
|
||||
|
||||
public override string ToString() => Filename;
|
||||
}
|
||||
|
||||
private class TestLegacyExporter : LegacyExporter<IHasNamedFiles>
|
||||
{
|
||||
public TestLegacyExporter(Storage storage)
|
||||
public TestLegacyModelExporter(Storage storage)
|
||||
: base(storage)
|
||||
{
|
||||
}
|
||||
|
||||
public string GetExtension() => FileExtension;
|
||||
|
||||
public override void ExportToStream(TestModel model, Stream outputStream, ProgressNotification? notification, CancellationToken cancellationToken = default)
|
||||
{
|
||||
}
|
||||
|
||||
protected override string FileExtension => ".test";
|
||||
}
|
||||
|
||||
private class TestModel : RealmObject, IHasNamedFiles, IHasGuidPrimaryKey
|
||||
{
|
||||
public Guid ID => Guid.Empty;
|
||||
|
||||
public string Filename { get; }
|
||||
|
||||
public IEnumerable<INamedFileUsage> Files { get; } = new List<INamedFileUsage>();
|
||||
|
||||
public TestModel(string filename)
|
||||
{
|
||||
Filename = filename;
|
||||
}
|
||||
|
||||
public override string ToString() => Filename;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,45 +37,6 @@ namespace osu.Game.Tests.Editing.Checks
|
||||
cpi.Add(2000, new SampleControlPoint { SampleVolume = volume_muted });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNormalControlPointVolume()
|
||||
{
|
||||
var hitCircle = new HitCircle
|
||||
{
|
||||
StartTime = 0,
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
|
||||
};
|
||||
hitCircle.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
assertOk(new List<HitObject> { hitCircle });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLowControlPointVolume()
|
||||
{
|
||||
var hitCircle = new HitCircle
|
||||
{
|
||||
StartTime = 1000,
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
|
||||
};
|
||||
hitCircle.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
assertLowVolume(new List<HitObject> { hitCircle });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMutedControlPointVolume()
|
||||
{
|
||||
var hitCircle = new HitCircle
|
||||
{
|
||||
StartTime = 2000,
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
|
||||
};
|
||||
hitCircle.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
assertMuted(new List<HitObject> { hitCircle });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNormalSampleVolume()
|
||||
{
|
||||
@@ -122,7 +83,7 @@ namespace osu.Game.Tests.Editing.Checks
|
||||
var sliderHead = new SliderHeadCircle
|
||||
{
|
||||
StartTime = 0,
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_regular) }
|
||||
};
|
||||
sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
@@ -135,7 +96,7 @@ namespace osu.Game.Tests.Editing.Checks
|
||||
|
||||
var slider = new MockNestableHitObject(new List<HitObject> { sliderHead, sliderTick, }, startTime: 0, endTime: 500)
|
||||
{
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_regular) }
|
||||
};
|
||||
slider.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
@@ -155,13 +116,13 @@ namespace osu.Game.Tests.Editing.Checks
|
||||
var sliderTick = new SliderTick
|
||||
{
|
||||
StartTime = 250,
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo("slidertick") }
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo("slidertick", volume: volume_regular) }
|
||||
};
|
||||
sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
var slider = new MockNestableHitObject(new List<HitObject> { sliderHead, sliderTick, }, startTime: 0, endTime: 500)
|
||||
{
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } // Applies to the tail.
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_regular) } // Applies to the tail.
|
||||
};
|
||||
slider.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
@@ -174,14 +135,14 @@ namespace osu.Game.Tests.Editing.Checks
|
||||
var sliderHead = new SliderHeadCircle
|
||||
{
|
||||
StartTime = 0,
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_regular) }
|
||||
};
|
||||
sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
var sliderTick = new SliderTick
|
||||
{
|
||||
StartTime = 250,
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo("slidertick") }
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo("slidertick", volume: volume_regular) }
|
||||
};
|
||||
sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
@@ -194,59 +155,6 @@ namespace osu.Game.Tests.Editing.Checks
|
||||
assertMutedPassive(new List<HitObject> { slider });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMutedControlPointVolumeSliderHead()
|
||||
{
|
||||
var sliderHead = new SliderHeadCircle
|
||||
{
|
||||
StartTime = 2000,
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
|
||||
};
|
||||
sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
var sliderTick = new SliderTick
|
||||
{
|
||||
StartTime = 2250,
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo("slidertick") }
|
||||
};
|
||||
sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
var slider = new MockNestableHitObject(new List<HitObject> { sliderHead, sliderTick, }, startTime: 2000, endTime: 2500)
|
||||
{
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_regular) }
|
||||
};
|
||||
slider.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
assertMuted(new List<HitObject> { slider });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMutedControlPointVolumeSliderTail()
|
||||
{
|
||||
var sliderHead = new SliderHeadCircle
|
||||
{
|
||||
StartTime = 0,
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
|
||||
};
|
||||
sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
var sliderTick = new SliderTick
|
||||
{
|
||||
StartTime = 250,
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo("slidertick") }
|
||||
};
|
||||
sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
// Ends after the 5% control point.
|
||||
var slider = new MockNestableHitObject(new List<HitObject> { sliderHead, sliderTick, }, startTime: 0, endTime: 2500)
|
||||
{
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
|
||||
};
|
||||
slider.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
assertMutedPassive(new List<HitObject> { slider });
|
||||
}
|
||||
|
||||
private void assertOk(List<HitObject> hitObjects)
|
||||
{
|
||||
Assert.That(check.Run(getContext(hitObjects)), Is.Empty);
|
||||
|
||||
@@ -14,6 +14,7 @@ using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||
using osu.Game.Rulesets.Osu.Edit;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
@@ -74,12 +75,9 @@ namespace osu.Game.Tests.Editing
|
||||
[TestCase(2)]
|
||||
public void TestSpeedMultiplierDoesNotChangeDistanceSnap(float multiplier)
|
||||
{
|
||||
assertSnapDistance(100, new HitObject
|
||||
assertSnapDistance(100, new Slider
|
||||
{
|
||||
DifficultyControlPoint = new DifficultyControlPoint
|
||||
{
|
||||
SliderVelocity = multiplier
|
||||
}
|
||||
SliderVelocity = multiplier
|
||||
}, false);
|
||||
}
|
||||
|
||||
@@ -87,12 +85,9 @@ namespace osu.Game.Tests.Editing
|
||||
[TestCase(2)]
|
||||
public void TestSpeedMultiplierDoesChangeDistanceSnap(float multiplier)
|
||||
{
|
||||
assertSnapDistance(100 * multiplier, new HitObject
|
||||
assertSnapDistance(100 * multiplier, new Slider
|
||||
{
|
||||
DifficultyControlPoint = new DifficultyControlPoint
|
||||
{
|
||||
SliderVelocity = multiplier
|
||||
}
|
||||
SliderVelocity = multiplier
|
||||
}, true);
|
||||
}
|
||||
|
||||
@@ -114,12 +109,9 @@ namespace osu.Game.Tests.Editing
|
||||
const float base_distance = 100;
|
||||
const float slider_velocity = 1.2f;
|
||||
|
||||
var referenceObject = new HitObject
|
||||
var referenceObject = new Slider
|
||||
{
|
||||
DifficultyControlPoint = new DifficultyControlPoint
|
||||
{
|
||||
SliderVelocity = slider_velocity
|
||||
}
|
||||
SliderVelocity = slider_velocity
|
||||
};
|
||||
|
||||
assertSnapDistance(base_distance * slider_velocity, referenceObject, true);
|
||||
|
||||
@@ -49,5 +49,31 @@ namespace osu.Game.Tests.Mods
|
||||
Assert.That(mod3, Is.EqualTo(mod2));
|
||||
Assert.That(doubleConvertedMod3, Is.EqualTo(doubleConvertedMod2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestModWithMultipleSettings()
|
||||
{
|
||||
var ruleset = new OsuRuleset();
|
||||
|
||||
var mod1 = new OsuModDifficultyAdjust { OverallDifficulty = { Value = 10 }, CircleSize = { Value = 0 } };
|
||||
var mod2 = new OsuModDifficultyAdjust { OverallDifficulty = { Value = 10 }, CircleSize = { Value = 6 } };
|
||||
var mod3 = new OsuModDifficultyAdjust { OverallDifficulty = { Value = 10 }, CircleSize = { Value = 6 } };
|
||||
|
||||
var doubleConvertedMod1 = new APIMod(mod1).ToMod(ruleset);
|
||||
var doubleConvertedMod2 = new APIMod(mod2).ToMod(ruleset);
|
||||
var doubleConvertedMod3 = new APIMod(mod3).ToMod(ruleset);
|
||||
|
||||
Assert.That(mod1, Is.Not.EqualTo(mod2));
|
||||
Assert.That(doubleConvertedMod1, Is.Not.EqualTo(doubleConvertedMod2));
|
||||
|
||||
Assert.That(mod2, Is.EqualTo(mod2));
|
||||
Assert.That(doubleConvertedMod2, Is.EqualTo(doubleConvertedMod2));
|
||||
|
||||
Assert.That(mod2, Is.EqualTo(mod3));
|
||||
Assert.That(doubleConvertedMod2, Is.EqualTo(doubleConvertedMod3));
|
||||
|
||||
Assert.That(mod3, Is.EqualTo(mod2));
|
||||
Assert.That(doubleConvertedMod3, Is.EqualTo(doubleConvertedMod2));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
|
||||
@@ -10,7 +13,7 @@ namespace osu.Game.Tests.Mods
|
||||
public class ModSettingsTest
|
||||
{
|
||||
[Test]
|
||||
public void TestModSettingsUnboundWhenCopied()
|
||||
public void TestModSettingsUnboundWhenCloned()
|
||||
{
|
||||
var original = new OsuModDoubleTime();
|
||||
var copy = (OsuModDoubleTime)original.DeepClone();
|
||||
@@ -22,7 +25,7 @@ namespace osu.Game.Tests.Mods
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMultiModSettingsUnboundWhenCopied()
|
||||
public void TestMultiModSettingsUnboundWhenCloned()
|
||||
{
|
||||
var original = new MultiMod(new OsuModDoubleTime());
|
||||
var copy = (MultiMod)original.DeepClone();
|
||||
@@ -32,5 +35,67 @@ namespace osu.Game.Tests.Mods
|
||||
Assert.That(((OsuModDoubleTime)original.Mods[0]).SpeedChange.Value, Is.EqualTo(2.0));
|
||||
Assert.That(((OsuModDoubleTime)copy.Mods[0]).SpeedChange.Value, Is.EqualTo(1.5));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDifferentTypeSettingsKeptWhenCopied()
|
||||
{
|
||||
const double setting_change = 50.4;
|
||||
|
||||
var modDouble = new TestNonMatchingSettingTypeModDouble { TestSetting = { Value = setting_change } };
|
||||
var modBool = new TestNonMatchingSettingTypeModBool { TestSetting = { Default = false, Value = true } };
|
||||
var modInt = new TestNonMatchingSettingTypeModInt { TestSetting = { Value = (int)setting_change / 2 } };
|
||||
|
||||
modDouble.CopyCommonSettingsFrom(modBool);
|
||||
modDouble.CopyCommonSettingsFrom(modInt);
|
||||
modBool.CopyCommonSettingsFrom(modDouble);
|
||||
modBool.CopyCommonSettingsFrom(modInt);
|
||||
modInt.CopyCommonSettingsFrom(modDouble);
|
||||
modInt.CopyCommonSettingsFrom(modBool);
|
||||
|
||||
Assert.That(modDouble.TestSetting.Value, Is.EqualTo(setting_change));
|
||||
Assert.That(modBool.TestSetting.Value, Is.EqualTo(true));
|
||||
Assert.That(modInt.TestSetting.Value, Is.EqualTo((int)setting_change / 2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDefaultValueKeptWhenCopied()
|
||||
{
|
||||
var modBoolTrue = new TestNonMatchingSettingTypeModBool { TestSetting = { Default = true, Value = false } };
|
||||
var modBoolFalse = new TestNonMatchingSettingTypeModBool { TestSetting = { Default = false, Value = true } };
|
||||
|
||||
modBoolFalse.CopyCommonSettingsFrom(modBoolTrue);
|
||||
|
||||
Assert.That(modBoolFalse.TestSetting.Default, Is.EqualTo(false));
|
||||
Assert.That(modBoolFalse.TestSetting.Value, Is.EqualTo(modBoolTrue.TestSetting.Value));
|
||||
}
|
||||
|
||||
private class TestNonMatchingSettingTypeModDouble : TestNonMatchingSettingTypeMod
|
||||
{
|
||||
public override string Acronym => "NMD";
|
||||
public override BindableNumber<double> TestSetting { get; } = new BindableDouble();
|
||||
}
|
||||
|
||||
private class TestNonMatchingSettingTypeModInt : TestNonMatchingSettingTypeMod
|
||||
{
|
||||
public override string Acronym => "NMI";
|
||||
public override BindableNumber<int> TestSetting { get; } = new BindableInt();
|
||||
}
|
||||
|
||||
private class TestNonMatchingSettingTypeModBool : TestNonMatchingSettingTypeMod
|
||||
{
|
||||
public override string Acronym => "NMB";
|
||||
public override Bindable<bool> TestSetting { get; } = new BindableBool();
|
||||
}
|
||||
|
||||
private abstract class TestNonMatchingSettingTypeMod : Mod
|
||||
{
|
||||
public override string Name => "Non-matching setting type mod";
|
||||
public override LocalisableString Description => "Description";
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override ModType Type => ModType.Conversion;
|
||||
|
||||
[SettingSource("Test setting")]
|
||||
public abstract IBindable TestSetting { get; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
#include "sh_Utils.h"
|
||||
#define HIGH_PRECISION_VERTEX
|
||||
|
||||
varying mediump vec2 v_TexCoord;
|
||||
varying mediump vec4 v_TexRect;
|
||||
#include "sh_Utils.h"
|
||||
#include "sh_Masking.h"
|
||||
|
||||
layout(location = 2) in highp vec2 v_TexCoord;
|
||||
|
||||
layout(location = 0) out vec4 o_Colour;
|
||||
|
||||
void main(void)
|
||||
{
|
||||
float hueValue = v_TexCoord.x / (v_TexRect[2] - v_TexRect[0]);
|
||||
gl_FragColor = hsv2rgb(vec4(hueValue, 1, 1, 1));
|
||||
highp float hueValue = v_TexCoord.x / (v_TexRect[2] - v_TexRect[0]);
|
||||
o_Colour = getRoundedColor(hsv2rgb(vec4(hueValue, 1, 1, 1)), v_TexCoord);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,31 +1,25 @@
|
||||
#include "sh_Utils.h"
|
||||
layout(location = 0) in highp vec2 m_Position;
|
||||
layout(location = 1) in lowp vec4 m_Colour;
|
||||
layout(location = 2) in highp vec2 m_TexCoord;
|
||||
layout(location = 3) in highp vec4 m_TexRect;
|
||||
layout(location = 4) in mediump vec2 m_BlendRange;
|
||||
|
||||
attribute highp vec2 m_Position;
|
||||
attribute lowp vec4 m_Colour;
|
||||
attribute mediump vec2 m_TexCoord;
|
||||
attribute mediump vec4 m_TexRect;
|
||||
attribute mediump vec2 m_BlendRange;
|
||||
|
||||
varying highp vec2 v_MaskingPosition;
|
||||
varying lowp vec4 v_Colour;
|
||||
varying mediump vec2 v_TexCoord;
|
||||
varying mediump vec4 v_TexRect;
|
||||
varying mediump vec2 v_BlendRange;
|
||||
|
||||
uniform highp mat4 g_ProjMatrix;
|
||||
uniform highp mat3 g_ToMaskingSpace;
|
||||
layout(location = 0) out highp vec2 v_MaskingPosition;
|
||||
layout(location = 1) out lowp vec4 v_Colour;
|
||||
layout(location = 2) out highp vec2 v_TexCoord;
|
||||
layout(location = 3) out highp vec4 v_TexRect;
|
||||
layout(location = 4) out mediump vec2 v_BlendRange;
|
||||
|
||||
void main(void)
|
||||
{
|
||||
// Transform from screen space to masking space.
|
||||
highp vec3 maskingPos = g_ToMaskingSpace * vec3(m_Position, 1.0);
|
||||
v_MaskingPosition = maskingPos.xy / maskingPos.z;
|
||||
// Transform from screen space to masking space.
|
||||
highp vec3 maskingPos = g_ToMaskingSpace * vec3(m_Position, 1.0);
|
||||
v_MaskingPosition = maskingPos.xy / maskingPos.z;
|
||||
|
||||
v_Colour = m_Colour;
|
||||
v_TexCoord = m_TexCoord;
|
||||
v_TexRect = m_TexRect;
|
||||
v_BlendRange = m_BlendRange;
|
||||
v_Colour = m_Colour;
|
||||
v_TexCoord = m_TexCoord;
|
||||
v_TexRect = m_TexRect;
|
||||
v_BlendRange = m_BlendRange;
|
||||
|
||||
gl_Position = gProjMatrix * vec4(m_Position, 1.0, 1.0);
|
||||
gl_Position = g_ProjMatrix * vec4(m_Position, 1.0, 1.0);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Extensions;
|
||||
@@ -120,10 +119,7 @@ namespace osu.Game.Tests.Skins.IO
|
||||
var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 1", "author 1"), "custom.osk"));
|
||||
assertCorrectMetadata(import1, "name 1 [custom]", "author 1", osu);
|
||||
|
||||
import1.PerformRead(s =>
|
||||
{
|
||||
new LegacySkinExporter(osu.Dependencies.Get<Storage>()).ExportModelTo(s, exportStream);
|
||||
});
|
||||
await new LegacySkinExporter(osu.Dependencies.Get<Storage>()).ExportToStreamAsync(import1, exportStream);
|
||||
|
||||
string exportFilename = import1.GetDisplayString();
|
||||
|
||||
@@ -141,10 +137,7 @@ namespace osu.Game.Tests.Skins.IO
|
||||
var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 『1』", "author 1"), "custom.osk"));
|
||||
assertCorrectMetadata(import1, "name 『1』 [custom]", "author 1", osu);
|
||||
|
||||
import1.PerformRead(s =>
|
||||
{
|
||||
new LegacySkinExporter(osu.Dependencies.Get<Storage>()).ExportModelTo(s, exportStream);
|
||||
});
|
||||
await new LegacySkinExporter(osu.Dependencies.Get<Storage>()).ExportToStreamAsync(import1, exportStream);
|
||||
|
||||
string exportFilename = import1.GetDisplayString().GetValidFilename();
|
||||
|
||||
@@ -208,7 +201,7 @@ namespace osu.Game.Tests.Skins.IO
|
||||
});
|
||||
|
||||
[Test]
|
||||
public Task TestExportThenImportDefaultSkin() => runSkinTest(osu =>
|
||||
public Task TestExportThenImportDefaultSkin() => runSkinTest(async osu =>
|
||||
{
|
||||
var skinManager = osu.Dependencies.Get<SkinManager>();
|
||||
|
||||
@@ -218,30 +211,28 @@ namespace osu.Game.Tests.Skins.IO
|
||||
|
||||
Guid originalSkinId = skinManager.CurrentSkinInfo.Value.ID;
|
||||
|
||||
skinManager.CurrentSkinInfo.Value.PerformRead(s =>
|
||||
await skinManager.CurrentSkinInfo.Value.PerformRead(async s =>
|
||||
{
|
||||
Assert.IsFalse(s.Protected);
|
||||
Assert.AreEqual(typeof(ArgonSkin), s.CreateInstance(skinManager).GetType());
|
||||
|
||||
new LegacySkinExporter(osu.Dependencies.Get<Storage>()).ExportModelTo(s, exportStream);
|
||||
await new LegacySkinExporter(osu.Dependencies.Get<Storage>()).ExportToStreamAsync(skinManager.CurrentSkinInfo.Value, exportStream);
|
||||
|
||||
Assert.Greater(exportStream.Length, 0);
|
||||
});
|
||||
|
||||
var imported = skinManager.Import(new ImportTask(exportStream, "exported.osk"));
|
||||
var imported = await skinManager.Import(new ImportTask(exportStream, "exported.osk"));
|
||||
|
||||
imported.GetResultSafely().PerformRead(s =>
|
||||
imported.PerformRead(s =>
|
||||
{
|
||||
Assert.IsFalse(s.Protected);
|
||||
Assert.AreNotEqual(originalSkinId, s.ID);
|
||||
Assert.AreEqual(typeof(ArgonSkin), s.CreateInstance(skinManager).GetType());
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
[Test]
|
||||
public Task TestExportThenImportClassicSkin() => runSkinTest(osu =>
|
||||
public Task TestExportThenImportClassicSkin() => runSkinTest(async osu =>
|
||||
{
|
||||
var skinManager = osu.Dependencies.Get<SkinManager>();
|
||||
|
||||
@@ -253,26 +244,24 @@ namespace osu.Game.Tests.Skins.IO
|
||||
|
||||
Guid originalSkinId = skinManager.CurrentSkinInfo.Value.ID;
|
||||
|
||||
skinManager.CurrentSkinInfo.Value.PerformRead(s =>
|
||||
await skinManager.CurrentSkinInfo.Value.PerformRead(async s =>
|
||||
{
|
||||
Assert.IsFalse(s.Protected);
|
||||
Assert.AreEqual(typeof(DefaultLegacySkin), s.CreateInstance(skinManager).GetType());
|
||||
|
||||
new LegacySkinExporter(osu.Dependencies.Get<Storage>()).ExportModelTo(s, exportStream);
|
||||
await new LegacySkinExporter(osu.Dependencies.Get<Storage>()).ExportToStreamAsync(skinManager.CurrentSkinInfo.Value, exportStream);
|
||||
|
||||
Assert.Greater(exportStream.Length, 0);
|
||||
});
|
||||
|
||||
var imported = skinManager.Import(new ImportTask(exportStream, "exported.osk"));
|
||||
var imported = await skinManager.Import(new ImportTask(exportStream, "exported.osk"));
|
||||
|
||||
imported.GetResultSafely().PerformRead(s =>
|
||||
imported.PerformRead(s =>
|
||||
{
|
||||
Assert.IsFalse(s.Protected);
|
||||
Assert.AreNotEqual(originalSkinId, s.ID);
|
||||
Assert.AreEqual(typeof(DefaultLegacySkin), s.CreateInstance(skinManager).GetType());
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -53,6 +53,8 @@ namespace osu.Game.Tests.Testing
|
||||
{
|
||||
Dependencies.Get<ShaderManager>().GetRawData(@"sh_TestVertex.vs");
|
||||
Dependencies.Get<ShaderManager>().GetRawData(@"sh_TestFragment.fs");
|
||||
Dependencies.Get<ShaderManager>().Load(@"TestVertex", @"TestFragment");
|
||||
Dependencies.Get<ShaderManager>().Load(VertexShaderDescriptor.TEXTURE_2, @"TestFragment");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
@@ -95,10 +94,6 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
var path = slider.Path;
|
||||
return path.ControlPoints.Count == 2 && path.ControlPoints.SequenceEqual(addedObject.Path.ControlPoints);
|
||||
});
|
||||
|
||||
// see `HitObject.control_point_leniency`.
|
||||
AddAssert("sample control point has correct time", () => Precision.AlmostEquals(slider.SampleControlPoint.Time, slider.GetEndTime(), 1));
|
||||
AddAssert("difficulty control point has correct time", () => slider.DifficultyControlPoint.Time == slider.StartTime);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -122,19 +122,9 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
AddAssert("Beatmap has correct timing point", () => EditorBeatmap.ControlPointInfo.TimingPoints.Single().Time == 500);
|
||||
|
||||
// After placement these must be non-default as defaults are read-only.
|
||||
AddAssert("Placed object has non-default control points", () =>
|
||||
!ReferenceEquals(EditorBeatmap.HitObjects[0].SampleControlPoint, SampleControlPoint.DEFAULT) &&
|
||||
!ReferenceEquals(EditorBeatmap.HitObjects[0].DifficultyControlPoint, DifficultyControlPoint.DEFAULT));
|
||||
|
||||
ReloadEditorToSameBeatmap();
|
||||
|
||||
AddAssert("Beatmap still has correct timing point", () => EditorBeatmap.ControlPointInfo.TimingPoints.Single().Time == 500);
|
||||
|
||||
// After placement these must be non-default as defaults are read-only.
|
||||
AddAssert("Placed object still has non-default control points", () =>
|
||||
!ReferenceEquals(EditorBeatmap.HitObjects[0].SampleControlPoint, SampleControlPoint.DEFAULT) &&
|
||||
!ReferenceEquals(EditorBeatmap.HitObjects[0].DifficultyControlPoint, DifficultyControlPoint.DEFAULT));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -108,12 +108,16 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
AddStep("Change to hitcircle", () => hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "HitCircle").TriggerClick());
|
||||
|
||||
ExpandingToolboxContainer toolboxContainer = null!;
|
||||
|
||||
AddStep("move mouse to toolbox", () => InputManager.MoveMouseTo(toolboxContainer = hitObjectComposer.ChildrenOfType<ExpandingToolboxContainer>().First()));
|
||||
AddUntilStep("toolbox is expanded", () => toolboxContainer.Expanded.Value);
|
||||
AddUntilStep("wait for toolbox to expand", () => toolboxContainer.LatestTransformEndTime, () => Is.EqualTo(Time.Current));
|
||||
|
||||
AddStep("move mouse to overlapping toggle button", () =>
|
||||
{
|
||||
var playfield = hitObjectComposer.Playfield.ScreenSpaceDrawQuad;
|
||||
var button = hitObjectComposer
|
||||
.ChildrenOfType<ExpandingToolboxContainer>().First()
|
||||
.ChildrenOfType<DrawableTernaryButton>().First(b => playfield.Contains(b.ScreenSpaceDrawQuad.Centre));
|
||||
var button = toolboxContainer.ChildrenOfType<DrawableTernaryButton>().First(b => playfield.Contains(b.ScreenSpaceDrawQuad.Centre));
|
||||
|
||||
InputManager.MoveMouseTo(button);
|
||||
});
|
||||
|
||||
@@ -8,10 +8,10 @@ using Humanizer;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
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.Objects;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
@@ -61,10 +61,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
new PathControlPoint(new Vector2(100, 0))
|
||||
}
|
||||
},
|
||||
DifficultyControlPoint = new DifficultyControlPoint
|
||||
{
|
||||
SliderVelocity = 2
|
||||
}
|
||||
SliderVelocity = 2
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -95,13 +92,27 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
hitObjectHasVelocity(1, 5);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUndo()
|
||||
{
|
||||
clickDifficultyPiece(1);
|
||||
velocityPopoverHasSingleValue(2);
|
||||
|
||||
setVelocityViaPopover(5);
|
||||
hitObjectHasVelocity(1, 5);
|
||||
dismissPopover();
|
||||
|
||||
AddStep("undo", () => Editor.Undo());
|
||||
hitObjectHasVelocity(1, 2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMultipleSelectionWithSameSliderVelocity()
|
||||
{
|
||||
AddStep("unify slider velocity", () =>
|
||||
{
|
||||
foreach (var h in EditorBeatmap.HitObjects)
|
||||
h.DifficultyControlPoint.SliderVelocity = 1.5;
|
||||
foreach (var h in EditorBeatmap.HitObjects.OfType<IHasSliderVelocity>())
|
||||
h.SliderVelocity = 1.5;
|
||||
});
|
||||
|
||||
AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
|
||||
@@ -185,7 +196,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
private void hitObjectHasVelocity(int objectIndex, double velocity) => AddAssert($"{objectIndex.ToOrdinalWords()} has velocity {velocity}", () =>
|
||||
{
|
||||
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex);
|
||||
return h.DifficultyControlPoint.SliderVelocity == velocity;
|
||||
return h is IHasSliderVelocity hasSliderVelocity && hasSliderVelocity.SliderVelocity == velocity;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+62
-12
@@ -4,11 +4,12 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using Humanizer;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Rulesets;
|
||||
@@ -23,7 +24,7 @@ using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Editing
|
||||
{
|
||||
public partial class TestSceneHitObjectSamplePointAdjustments : EditorTestScene
|
||||
public partial class TestSceneHitObjectSampleAdjustments : EditorTestScene
|
||||
{
|
||||
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
|
||||
|
||||
@@ -39,10 +40,9 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
{
|
||||
StartTime = 0,
|
||||
Position = (OsuPlayfield.BASE_SIZE - new Vector2(100, 0)) / 2,
|
||||
SampleControlPoint = new SampleControlPoint
|
||||
Samples = new List<HitSampleInfo>
|
||||
{
|
||||
SampleBank = "normal",
|
||||
SampleVolume = 80
|
||||
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: 80)
|
||||
}
|
||||
});
|
||||
|
||||
@@ -50,15 +50,34 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
{
|
||||
StartTime = 500,
|
||||
Position = (OsuPlayfield.BASE_SIZE + new Vector2(100, 0)) / 2,
|
||||
SampleControlPoint = new SampleControlPoint
|
||||
Samples = new List<HitSampleInfo>
|
||||
{
|
||||
SampleBank = "soft",
|
||||
SampleVolume = 60
|
||||
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "soft", volume: 60)
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAddSampleAddition()
|
||||
{
|
||||
AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
|
||||
|
||||
AddStep("add clap addition", () => InputManager.Key(Key.R));
|
||||
|
||||
hitObjectHasSampleBank(0, "normal");
|
||||
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP);
|
||||
hitObjectHasSampleBank(1, "soft");
|
||||
hitObjectHasSamples(1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP);
|
||||
|
||||
AddStep("remove clap addition", () => InputManager.Key(Key.R));
|
||||
|
||||
hitObjectHasSampleBank(0, "normal");
|
||||
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL);
|
||||
hitObjectHasSampleBank(1, "soft");
|
||||
hitObjectHasSamples(1, HitSampleInfo.HIT_NORMAL);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPopoverHasFocus()
|
||||
{
|
||||
@@ -90,13 +109,33 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
hitObjectHasSampleBank(1, "drum");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUndo()
|
||||
{
|
||||
clickSamplePiece(1);
|
||||
samplePopoverHasSingleBank("soft");
|
||||
samplePopoverHasSingleVolume(60);
|
||||
|
||||
setVolumeViaPopover(90);
|
||||
hitObjectHasSampleVolume(1, 90);
|
||||
dismissPopover();
|
||||
|
||||
AddStep("undo", () => Editor.Undo());
|
||||
hitObjectHasSampleVolume(1, 60);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMultipleSelectionWithSameSampleVolume()
|
||||
{
|
||||
AddStep("unify sample volume", () =>
|
||||
{
|
||||
foreach (var h in EditorBeatmap.HitObjects)
|
||||
h.SampleControlPoint.SampleVolume = 50;
|
||||
{
|
||||
for (int i = 0; i < h.Samples.Count; i++)
|
||||
{
|
||||
h.Samples[i] = h.Samples[i].With(newVolume: 50);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
|
||||
@@ -136,7 +175,12 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
AddStep("unify sample bank", () =>
|
||||
{
|
||||
foreach (var h in EditorBeatmap.HitObjects)
|
||||
h.SampleControlPoint.SampleBank = "soft";
|
||||
{
|
||||
for (int i = 0; i < h.Samples.Count; i++)
|
||||
{
|
||||
h.Samples[i] = h.Samples[i].With(newBank: "soft");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
|
||||
@@ -248,7 +292,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
private void hitObjectHasSampleVolume(int objectIndex, int volume) => AddAssert($"{objectIndex.ToOrdinalWords()} has volume {volume}", () =>
|
||||
{
|
||||
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex);
|
||||
return h.SampleControlPoint.SampleVolume == volume;
|
||||
return h.Samples.All(o => o.Volume == volume);
|
||||
});
|
||||
|
||||
private void setBankViaPopover(string bank) => AddStep($"set bank {bank} via popover", () =>
|
||||
@@ -262,10 +306,16 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
InputManager.Key(Key.Enter);
|
||||
});
|
||||
|
||||
private void hitObjectHasSamples(int objectIndex, params string[] samples) => AddAssert($"{objectIndex.ToOrdinalWords()} has samples {string.Join(',', samples)}", () =>
|
||||
{
|
||||
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex);
|
||||
return h.Samples.Select(s => s.Name).SequenceEqual(samples);
|
||||
});
|
||||
|
||||
private void hitObjectHasSampleBank(int objectIndex, string bank) => AddAssert($"{objectIndex.ToOrdinalWords()} has bank {bank}", () =>
|
||||
{
|
||||
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex);
|
||||
return h.SampleControlPoint.SampleBank == bank;
|
||||
return h.Samples.All(o => o.Bank == bank);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// 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.Extensions;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Tests.Resources;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Editing
|
||||
{
|
||||
public partial class TestSceneLocallyModifyingOnlineBeatmaps : EditorSavingTestScene
|
||||
{
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
CreateInitialBeatmap = () =>
|
||||
{
|
||||
var importedSet = Game.BeatmapManager.Import(new ImportTask(TestResources.GetTestBeatmapForImport())).GetResultSafely();
|
||||
return Game.BeatmapManager.GetWorkingBeatmap(importedSet!.Value.Beatmaps.First());
|
||||
};
|
||||
|
||||
base.SetUpSteps();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLocallyModifyingOnlineBeatmap()
|
||||
{
|
||||
AddAssert("editor beatmap has online ID", () => EditorBeatmap.BeatmapInfo.OnlineID, () => Is.GreaterThan(0));
|
||||
|
||||
AddStep("delete first hitobject", () => EditorBeatmap.RemoveAt(0));
|
||||
SaveEditor();
|
||||
|
||||
ReloadEditorToSameBeatmap();
|
||||
AddAssert("editor beatmap online ID reset", () => EditorBeatmap.BeatmapInfo.OnlineID, () => Is.EqualTo(-1));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
// 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.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Editing
|
||||
{
|
||||
public partial class TestScenePlacementBlueprint : EditorTestScene
|
||||
{
|
||||
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
|
||||
|
||||
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
|
||||
|
||||
private GlobalActionContainer globalActionContainer => this.ChildrenOfType<GlobalActionContainer>().Single();
|
||||
|
||||
[Test]
|
||||
public void TestCommitPlacementViaGlobalAction()
|
||||
{
|
||||
Playfield playfield = null!;
|
||||
|
||||
AddStep("select slider placement tool", () => InputManager.Key(Key.Number3));
|
||||
AddStep("move mouse to top left of playfield", () =>
|
||||
{
|
||||
playfield = this.ChildrenOfType<Playfield>().Single();
|
||||
var location = (3 * playfield.ScreenSpaceDrawQuad.TopLeft + playfield.ScreenSpaceDrawQuad.BottomRight) / 4;
|
||||
InputManager.MoveMouseTo(location);
|
||||
});
|
||||
AddStep("begin placement", () => InputManager.Click(MouseButton.Left));
|
||||
AddStep("move mouse to bottom right of playfield", () =>
|
||||
{
|
||||
var location = (playfield.ScreenSpaceDrawQuad.TopLeft + 3 * playfield.ScreenSpaceDrawQuad.BottomRight) / 4;
|
||||
InputManager.MoveMouseTo(location);
|
||||
});
|
||||
AddStep("confirm via global action", () =>
|
||||
{
|
||||
globalActionContainer.TriggerPressed(GlobalAction.Select);
|
||||
globalActionContainer.TriggerReleased(GlobalAction.Select);
|
||||
});
|
||||
AddAssert("slider placed", () => EditorBeatmap.HitObjects.Count, () => Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAbortPlacementViaGlobalAction()
|
||||
{
|
||||
Playfield playfield = null!;
|
||||
|
||||
AddStep("select slider placement tool", () => InputManager.Key(Key.Number3));
|
||||
AddStep("move mouse to top left of playfield", () =>
|
||||
{
|
||||
playfield = this.ChildrenOfType<Playfield>().Single();
|
||||
var location = (3 * playfield.ScreenSpaceDrawQuad.TopLeft + playfield.ScreenSpaceDrawQuad.BottomRight) / 4;
|
||||
InputManager.MoveMouseTo(location);
|
||||
});
|
||||
AddStep("begin placement", () => InputManager.Click(MouseButton.Left));
|
||||
AddStep("move mouse to bottom right of playfield", () =>
|
||||
{
|
||||
var location = (playfield.ScreenSpaceDrawQuad.TopLeft + 3 * playfield.ScreenSpaceDrawQuad.BottomRight) / 4;
|
||||
InputManager.MoveMouseTo(location);
|
||||
});
|
||||
AddStep("abort via global action", () =>
|
||||
{
|
||||
globalActionContainer.TriggerPressed(GlobalAction.Back);
|
||||
globalActionContainer.TriggerReleased(GlobalAction.Back);
|
||||
});
|
||||
AddAssert("editor is still current", () => Editor.IsCurrentScreen());
|
||||
AddAssert("slider not placed", () => EditorBeatmap.HitObjects.Count, () => Is.EqualTo(0));
|
||||
AddAssert("no active placement", () => this.ChildrenOfType<ComposeBlueprintContainer>().Single().CurrentPlacement.PlacementActive,
|
||||
() => Is.EqualTo(PlacementBlueprint.PlacementState.Waiting));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCommitPlacementViaToolChange()
|
||||
{
|
||||
Playfield playfield = null!;
|
||||
|
||||
AddStep("select slider placement tool", () => InputManager.Key(Key.Number3));
|
||||
AddStep("move mouse to top left of playfield", () =>
|
||||
{
|
||||
playfield = this.ChildrenOfType<Playfield>().Single();
|
||||
var location = (3 * playfield.ScreenSpaceDrawQuad.TopLeft + playfield.ScreenSpaceDrawQuad.BottomRight) / 4;
|
||||
InputManager.MoveMouseTo(location);
|
||||
});
|
||||
AddStep("begin placement", () => InputManager.Click(MouseButton.Left));
|
||||
AddStep("move mouse to bottom right of playfield", () =>
|
||||
{
|
||||
var location = (playfield.ScreenSpaceDrawQuad.TopLeft + 3 * playfield.ScreenSpaceDrawQuad.BottomRight) / 4;
|
||||
InputManager.MoveMouseTo(location);
|
||||
});
|
||||
|
||||
AddStep("change tool to circle", () => InputManager.Key(Key.Number2));
|
||||
AddAssert("slider placed", () => EditorBeatmap.HitObjects.Count, () => Is.EqualTo(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,5 +77,39 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
AddAssert("object has non-zero duration", () => EditorBeatmap.HitObjects.OfType<IHasDuration>().Single().Duration > 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDisallowRepeatsOnZeroDurationObjects()
|
||||
{
|
||||
DragArea dragArea;
|
||||
|
||||
AddStep("add zero length slider", () =>
|
||||
{
|
||||
EditorBeatmap.Clear();
|
||||
EditorBeatmap.Add(new Slider
|
||||
{
|
||||
Position = new Vector2(256, 256),
|
||||
StartTime = 2700
|
||||
});
|
||||
});
|
||||
|
||||
AddStep("hold down drag bar", () =>
|
||||
{
|
||||
// distinguishes between the actual drag bar and its "underlay shadow".
|
||||
dragArea = this.ChildrenOfType<DragArea>().Single(bar => bar.HandlePositionalInput);
|
||||
InputManager.MoveMouseTo(dragArea);
|
||||
InputManager.PressButton(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddStep("try to extend drag bar", () =>
|
||||
{
|
||||
var blueprint = this.ChildrenOfType<TimelineHitObjectBlueprint>().Single();
|
||||
InputManager.MoveMouseTo(blueprint.SelectionQuad.TopLeft + new Vector2(100, 0));
|
||||
});
|
||||
|
||||
AddStep("release button", () => InputManager.PressButton(MouseButton.Left));
|
||||
|
||||
AddAssert("object has zero repeats", () => EditorBeatmap.HitObjects.OfType<IHasRepeats>().Single().RepeatCount == 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,8 +73,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
new HitCircle
|
||||
{
|
||||
StartTime = t += spacing,
|
||||
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) },
|
||||
SampleControlPoint = new SampleControlPoint { SampleBank = "soft" },
|
||||
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "soft") },
|
||||
},
|
||||
new HitCircle
|
||||
{
|
||||
@@ -84,8 +83,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
StartTime = t += spacing,
|
||||
Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, Vector2.UnitY * 200 }),
|
||||
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE) },
|
||||
SampleControlPoint = new SampleControlPoint { SampleBank = "soft" },
|
||||
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE, "soft") },
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Settings;
|
||||
@@ -182,6 +185,64 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddAssert("all boxes still selected", () => skinEditor.SelectedComponents, () => Has.Count.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUndoEditHistory()
|
||||
{
|
||||
SkinComponentsContainer firstTarget = null!;
|
||||
TestSkinEditorChangeHandler changeHandler = null!;
|
||||
byte[] defaultState = null!;
|
||||
IEnumerable<ISerialisableDrawable> testComponents = null!;
|
||||
|
||||
AddStep("Load necessary things", () =>
|
||||
{
|
||||
firstTarget = Player.ChildrenOfType<SkinComponentsContainer>().First();
|
||||
changeHandler = new TestSkinEditorChangeHandler(firstTarget);
|
||||
|
||||
changeHandler.SaveState();
|
||||
defaultState = changeHandler.GetCurrentState();
|
||||
|
||||
testComponents = new[]
|
||||
{
|
||||
targetContainer.Components.First(),
|
||||
targetContainer.Components[targetContainer.Components.Count / 2],
|
||||
targetContainer.Components.Last()
|
||||
};
|
||||
});
|
||||
|
||||
AddStep("Press undo", () => InputManager.Keys(PlatformAction.Undo));
|
||||
AddAssert("Nothing changed", () => defaultState.SequenceEqual(changeHandler.GetCurrentState()));
|
||||
|
||||
AddStep("Add components", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(skinEditor.ChildrenOfType<BigBlackBox>().First());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
revertAndCheckUnchanged();
|
||||
|
||||
AddStep("Move components", () =>
|
||||
{
|
||||
changeHandler.BeginChange();
|
||||
testComponents.ForEach(c => ((Drawable)c).Position += Vector2.One);
|
||||
changeHandler.EndChange();
|
||||
});
|
||||
revertAndCheckUnchanged();
|
||||
|
||||
AddStep("Select components", () => skinEditor.SelectedComponents.AddRange(testComponents));
|
||||
AddStep("Bring to front", () => skinEditor.BringSelectionToFront());
|
||||
revertAndCheckUnchanged();
|
||||
|
||||
AddStep("Remove components", () => testComponents.ForEach(c => firstTarget.Remove(c, false)));
|
||||
revertAndCheckUnchanged();
|
||||
|
||||
void revertAndCheckUnchanged()
|
||||
{
|
||||
AddStep("Revert changes", () => changeHandler.RestoreState(int.MinValue));
|
||||
AddAssert("Current state is same as default", () => defaultState.SequenceEqual(changeHandler.GetCurrentState()));
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase(false)]
|
||||
[TestCase(true)]
|
||||
public void TestBringToFront(bool alterSelectionOrder)
|
||||
@@ -269,5 +330,23 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
}
|
||||
|
||||
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
|
||||
|
||||
private partial class TestSkinEditorChangeHandler : SkinEditorChangeHandler
|
||||
{
|
||||
public TestSkinEditorChangeHandler(Drawable targetScreen)
|
||||
: base(targetScreen)
|
||||
{
|
||||
}
|
||||
|
||||
public byte[] GetCurrentState()
|
||||
{
|
||||
using var stream = new MemoryStream();
|
||||
|
||||
WriteCurrentStateToStream(stream);
|
||||
byte[] newState = stream.ToArray();
|
||||
|
||||
return newState;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +98,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddStep($"select {requiredMod.ReadableName()} as required", () => songSelect.Mods.Value = new[] { (Mod)Activator.CreateInstance(requiredMod) });
|
||||
|
||||
AddAssert("freemods empty", () => songSelect.FreeMods.Value.Count == 0);
|
||||
|
||||
// A previous test's mod overlay could still be fading out.
|
||||
AddUntilStep("wait for only one freemod overlay", () => this.ChildrenOfType<FreeModSelectOverlay>().Count() == 1);
|
||||
|
||||
assertHasFreeModButton(allowedMod, false);
|
||||
assertHasFreeModButton(requiredMod, false);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
// 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.Screens;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Menu;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Navigation
|
||||
{
|
||||
public partial class TestSceneBeatmapEditorNavigation : OsuGameTestScene
|
||||
{
|
||||
/// <summary>
|
||||
/// When entering the editor, a new beatmap is created as part of the asynchronous load process.
|
||||
/// This test ensures that in the case of an early exit from the editor (ie. while it's still loading)
|
||||
/// doesn't leave a dangling beatmap behind.
|
||||
///
|
||||
/// This may not fail 100% due to timing, but has a pretty high chance of hitting a failure so works well enough
|
||||
/// as a test.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestCancelNavigationToEditor()
|
||||
{
|
||||
BeatmapSetInfo[] beatmapSets = null!;
|
||||
|
||||
AddStep("Fetch initial beatmaps", () => beatmapSets = allBeatmapSets());
|
||||
|
||||
AddStep("Set current beatmap to default", () => Game.Beatmap.SetDefault());
|
||||
|
||||
AddStep("Push editor loader", () => Game.ScreenStack.Push(new EditorLoader()));
|
||||
AddUntilStep("Wait for loader current", () => Game.ScreenStack.CurrentScreen is EditorLoader);
|
||||
AddStep("Close editor while loading", () => Game.ScreenStack.CurrentScreen.Exit());
|
||||
|
||||
AddUntilStep("Wait for menu", () => Game.ScreenStack.CurrentScreen is MainMenu);
|
||||
AddAssert("Check no new beatmaps were made", () => allBeatmapSets().SequenceEqual(beatmapSets));
|
||||
|
||||
BeatmapSetInfo[] allBeatmapSets() => Game.Realm.Run(realm => realm.All<BeatmapSetInfo>().Where(x => !x.DeletePending).ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -700,7 +700,7 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
AddStep("press escape twice rapidly", () =>
|
||||
{
|
||||
InputManager.Key(Key.Escape);
|
||||
InputManager.Key(Key.Escape);
|
||||
Schedule(InputManager.Key, Key.Escape);
|
||||
});
|
||||
|
||||
pushEscape();
|
||||
|
||||
@@ -8,10 +8,12 @@ using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input;
|
||||
@@ -30,6 +32,7 @@ using osu.Game.Overlays.Chat.Listing;
|
||||
using osu.Game.Overlays.Chat.ChannelList;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
@@ -53,6 +56,9 @@ namespace osu.Game.Tests.Visual.Online
|
||||
|
||||
private int currentMessageId;
|
||||
|
||||
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
|
||||
private readonly ManualResetEventSlim requestLock = new ManualResetEventSlim();
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
@@ -576,6 +582,75 @@ namespace osu.Game.Tests.Visual.Online
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestChatReport()
|
||||
{
|
||||
ChatReportRequest request = null;
|
||||
|
||||
AddStep("Show overlay with channel", () =>
|
||||
{
|
||||
chatOverlay.Show();
|
||||
channelManager.CurrentChannel.Value = channelManager.JoinChannel(testChannel1);
|
||||
});
|
||||
|
||||
AddAssert("Overlay is visible", () => chatOverlay.State.Value == Visibility.Visible);
|
||||
waitForChannel1Visible();
|
||||
|
||||
AddStep("Setup request handling", () =>
|
||||
{
|
||||
requestLock.Reset();
|
||||
|
||||
dummyAPI.HandleRequest = r =>
|
||||
{
|
||||
if (!(r is ChatReportRequest req))
|
||||
return false;
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
request = req;
|
||||
requestLock.Wait(10000);
|
||||
req.TriggerSuccess();
|
||||
});
|
||||
|
||||
return true;
|
||||
};
|
||||
});
|
||||
|
||||
AddStep("Show report popover", () => this.ChildrenOfType<ChatLine>().First().ShowPopover());
|
||||
|
||||
AddStep("Set report reason to other", () =>
|
||||
{
|
||||
var reason = this.ChildrenOfType<OsuEnumDropdown<ChatReportReason>>().Single();
|
||||
reason.Current.Value = ChatReportReason.Other;
|
||||
});
|
||||
|
||||
AddStep("Try to report", () =>
|
||||
{
|
||||
var btn = this.ChildrenOfType<ReportChatPopover>().Single().ChildrenOfType<RoundedButton>().Single();
|
||||
InputManager.MoveMouseTo(btn);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddAssert("Nothing happened", () => this.ChildrenOfType<ReportChatPopover>().Any());
|
||||
AddStep("Set report data", () =>
|
||||
{
|
||||
var field = this.ChildrenOfType<ReportChatPopover>().Single().ChildrenOfType<OsuTextBox>().Single();
|
||||
field.Current.Value = "test other";
|
||||
});
|
||||
|
||||
AddStep("Try to report", () =>
|
||||
{
|
||||
var btn = this.ChildrenOfType<ReportChatPopover>().Single().ChildrenOfType<RoundedButton>().Single();
|
||||
InputManager.MoveMouseTo(btn);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddUntilStep("Overlay closed", () => !this.ChildrenOfType<ReportChatPopover>().Any());
|
||||
AddStep("Complete request", () => requestLock.Set());
|
||||
AddUntilStep("Request sent", () => request != null);
|
||||
AddUntilStep("Info message displayed", () => channelManager.CurrentChannel.Value.Messages.Last(), () => Is.InstanceOf(typeof(InfoMessage)));
|
||||
}
|
||||
|
||||
private void joinTestChannel(int i)
|
||||
{
|
||||
AddStep($"Join test channel {i}", () => channelManager.JoinChannel(testChannels[i]));
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
@@ -20,7 +18,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
|
||||
|
||||
private WikiOverlay wiki;
|
||||
private WikiOverlay wiki = null!;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() => Child = wiki = new WikiOverlay());
|
||||
@@ -29,13 +27,13 @@ namespace osu.Game.Tests.Visual.Online
|
||||
public void TestMainPage()
|
||||
{
|
||||
setUpWikiResponse(responseMainPage);
|
||||
AddStep("Show main page", () => wiki.Show());
|
||||
AddStep("Show main page", () => wiki.ShowPage());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCancellationDoesntShowError()
|
||||
{
|
||||
AddStep("Show main page", () => wiki.Show());
|
||||
AddStep("Show main page", () => wiki.ShowPage());
|
||||
AddStep("Show another page", () => wiki.ShowPage("Article_styling_criteria/Formatting"));
|
||||
|
||||
AddUntilStep("Current path is not error", () => wiki.CurrentPath != "error");
|
||||
@@ -73,7 +71,23 @@ namespace osu.Game.Tests.Visual.Online
|
||||
AddUntilStep("Error message correct", () => wiki.ChildrenOfType<SpriteText>().Any(text => text.Text == "\"This_page_will_error_out\"."));
|
||||
}
|
||||
|
||||
private void setUpWikiResponse(APIWikiPage r, string redirectionPath = null)
|
||||
[Test]
|
||||
public void TestReturnAfterErrorPage()
|
||||
{
|
||||
setUpWikiResponse(responseArticlePage);
|
||||
|
||||
AddStep("Show article page", () => wiki.ShowPage("Article_styling_criteria/Formatting"));
|
||||
AddUntilStep("Wait for non-error page", () => wiki.CurrentPath == "Article_styling_criteria/Formatting");
|
||||
|
||||
AddStep("Show nonexistent page", () => wiki.ShowPage("This_page_will_error_out"));
|
||||
AddUntilStep("Wait for error page", () => wiki.CurrentPath == "error");
|
||||
|
||||
AddStep("Show article page", () => wiki.ShowPage("Article_styling_criteria/Formatting"));
|
||||
AddUntilStep("Wait for non-error page", () => wiki.CurrentPath == "Article_styling_criteria/Formatting");
|
||||
AddUntilStep("Error message not displayed", () => wiki.ChildrenOfType<SpriteText>().All(text => text.Text != "\"This_page_will_error_out\"."));
|
||||
}
|
||||
|
||||
private void setUpWikiResponse(APIWikiPage r, string? redirectionPath = null)
|
||||
=> AddStep("set up response", () =>
|
||||
{
|
||||
dummyAPI.HandleRequest = request =>
|
||||
|
||||
@@ -243,7 +243,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddUntilStep("wait for context menu", () => this.ChildrenOfType<OsuContextMenu>().Any());
|
||||
AddStep("click delete", () =>
|
||||
{
|
||||
var deleteItem = this.ChildrenOfType<DrawableOsuMenuItem>().Single();
|
||||
var deleteItem = this.ChildrenOfType<DrawableOsuMenuItem>().ElementAt(1);
|
||||
InputManager.MoveMouseTo(deleteItem);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
@@ -261,6 +261,137 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddAssert("preset soft-deleted", () => Realm.Run(r => r.All<ModPreset>().Count(preset => preset.DeletePending) == 1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEditPresetName()
|
||||
{
|
||||
ModPresetColumn modPresetColumn = null!;
|
||||
string presetName = null!;
|
||||
ModPresetPanel panel = null!;
|
||||
|
||||
AddStep("clear mods", () => SelectedMods.Value = Array.Empty<Mod>());
|
||||
AddStep("create content", () => Child = modPresetColumn = new ModPresetColumn
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
});
|
||||
|
||||
AddUntilStep("items loaded", () => modPresetColumn.IsLoaded && modPresetColumn.ItemsLoaded);
|
||||
AddStep("right click first panel", () =>
|
||||
{
|
||||
panel = this.ChildrenOfType<ModPresetPanel>().First();
|
||||
presetName = panel.Preset.Value.Name;
|
||||
InputManager.MoveMouseTo(panel);
|
||||
InputManager.Click(MouseButton.Right);
|
||||
});
|
||||
|
||||
AddUntilStep("wait for context menu", () => this.ChildrenOfType<OsuContextMenu>().Any());
|
||||
AddStep("click edit", () =>
|
||||
{
|
||||
var editItem = this.ChildrenOfType<DrawableOsuMenuItem>().ElementAt(0);
|
||||
InputManager.MoveMouseTo(editItem);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
OsuPopover? popover = null;
|
||||
AddUntilStep("wait for popover", () => (popover = this.ChildrenOfType<OsuPopover>().FirstOrDefault()) != null);
|
||||
AddStep("clear preset name", () => popover.ChildrenOfType<LabelledTextBox>().First().Current.Value = "");
|
||||
AddStep("attempt preset edit", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(popover.ChildrenOfType<ShearedButton>().ElementAt(1));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddAssert("preset is not changed", () => panel.Preset.Value.Name == presetName);
|
||||
AddUntilStep("popover is unchanged", () => this.ChildrenOfType<OsuPopover>().FirstOrDefault() == popover);
|
||||
AddStep("edit preset name", () => popover.ChildrenOfType<LabelledTextBox>().First().Current.Value = "something new");
|
||||
AddStep("attempt preset edit", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(popover.ChildrenOfType<ShearedButton>().ElementAt(1));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddUntilStep("popover closed", () => !this.ChildrenOfType<OsuPopover>().Any());
|
||||
AddAssert("preset is changed", () => panel.Preset.Value.Name != presetName);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEditPresetMod()
|
||||
{
|
||||
ModPresetColumn modPresetColumn = null!;
|
||||
var mods = new Mod[] { new OsuModHidden(), new OsuModHardRock() };
|
||||
List<Mod> previousMod = null!;
|
||||
|
||||
AddStep("clear mods", () => SelectedMods.Value = Array.Empty<Mod>());
|
||||
AddStep("create content", () => Child = modPresetColumn = new ModPresetColumn
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
});
|
||||
|
||||
AddUntilStep("items loaded", () => modPresetColumn.IsLoaded && modPresetColumn.ItemsLoaded);
|
||||
|
||||
AddStep("right click first panel", () =>
|
||||
{
|
||||
var panel = this.ChildrenOfType<ModPresetPanel>().First();
|
||||
previousMod = panel.Preset.Value.Mods.ToList();
|
||||
InputManager.MoveMouseTo(panel);
|
||||
InputManager.Click(MouseButton.Right);
|
||||
});
|
||||
AddUntilStep("wait for context menu", () => this.ChildrenOfType<OsuContextMenu>().Any());
|
||||
AddStep("click edit", () =>
|
||||
{
|
||||
var editItem = this.ChildrenOfType<DrawableOsuMenuItem>().ElementAt(0);
|
||||
InputManager.MoveMouseTo(editItem);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
OsuPopover? popover = null;
|
||||
AddUntilStep("wait for popover", () => (popover = this.ChildrenOfType<OsuPopover>().FirstOrDefault()) != null);
|
||||
AddStep("click use current mods", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(popover.ChildrenOfType<ShearedButton>().ElementAt(0));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddStep("attempt preset edit", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(popover.ChildrenOfType<ShearedButton>().ElementAt(1));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddUntilStep("preset mod not changed", () =>
|
||||
new HashSet<Mod>(this.ChildrenOfType<ModPresetPanel>().First().Preset.Value.Mods).SetEquals(previousMod));
|
||||
|
||||
AddStep("select mods", () => SelectedMods.Value = mods);
|
||||
AddStep("right click first panel", () =>
|
||||
{
|
||||
var panel = this.ChildrenOfType<ModPresetPanel>().First();
|
||||
InputManager.MoveMouseTo(panel);
|
||||
InputManager.Click(MouseButton.Right);
|
||||
});
|
||||
|
||||
AddUntilStep("wait for context menu", () => this.ChildrenOfType<OsuContextMenu>().Any());
|
||||
AddStep("click edit", () =>
|
||||
{
|
||||
var editItem = this.ChildrenOfType<DrawableOsuMenuItem>().ElementAt(0);
|
||||
InputManager.MoveMouseTo(editItem);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddUntilStep("wait for popover", () => (popover = this.ChildrenOfType<OsuPopover>().FirstOrDefault()) != null);
|
||||
AddStep("click use current mods", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(popover.ChildrenOfType<ShearedButton>().ElementAt(0));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddStep("attempt preset edit", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(popover.ChildrenOfType<ShearedButton>().ElementAt(1));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddUntilStep("preset mod is changed", () =>
|
||||
new HashSet<Mod>(this.ChildrenOfType<ModPresetPanel>().First().Preset.Value.Mods).SetEquals(mods));
|
||||
}
|
||||
|
||||
private ICollection<ModPreset> createTestPresets() => new[]
|
||||
{
|
||||
new ModPreset
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user