1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 20:13:21 +08:00

Merge branch 'master' into acc-challenge

This commit is contained in:
Bartłomiej Dach 2023-05-29 22:33:57 +02:00
commit 5604dfb758
No known key found for this signature in database
271 changed files with 4918 additions and 1756 deletions

View File

@ -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

View File

@ -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:

View File

@ -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 }}

View File

@ -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)

View File

@ -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)

7
global.json Normal file
View File

@ -0,0 +1,7 @@
{
"sdk": {
"version": "6.0.100",
"rollForward": "latestFeature"
}
}

View File

@ -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" />

View File

@ -9,6 +9,7 @@ using osu.Framework.Logging;
using osu.Game;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Screens.Play;
using Squirrel;
using Squirrel.SimpleSplat;
using LogLevel = Squirrel.SimpleSplat.LogLevel;
@ -36,6 +37,9 @@ namespace osu.Desktop.Updater
[Resolved]
private OsuGameBase game { get; set; } = null!;
[Resolved]
private ILocalUserPlayInfo? localUserInfo { get; set; }
[BackgroundDependencyLoader]
private void load(INotificationOverlay notifications)
{
@ -55,6 +59,10 @@ namespace osu.Desktop.Updater
try
{
// Avoid any kind of update checking while gameplay is running.
if (localUserInfo?.IsPlaying.Value == true)
return false;
updateManager ??= new GithubUpdateManager(@"https://github.com/ppy/osu", false, github_token, @"osulazer");
var info = await updateManager.CheckForUpdate(!useDeltaPatching).ConfigureAwait(false);

View File

@ -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]

View File

@ -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]

View File

@ -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)
})
});
}

View File

@ -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:

View File

@ -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;
}

View File

@ -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();

View File

@ -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;
}

View File

@ -132,7 +132,7 @@ namespace osu.Game.Rulesets.Catch.Edit
result.ScreenSpacePosition.X = screenSpacePosition.X;
if (snapType.HasFlagFast(SnapType.Grids))
if (snapType.HasFlagFast(SnapType.RelativeGrids))
{
if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult &&
Vector2.Distance(snapResult.ScreenSpacePosition, result.ScreenSpacePosition) < distance_snap_radius)

View File

@ -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)

View File

@ -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;

View File

@ -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"/>.

View File

@ -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)

View File

@ -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);

View File

@ -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;

View File

@ -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 }
}
};

View File

@ -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()

View File

@ -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,

View File

@ -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", () =>

View File

@ -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", HitSampleInfo.BANK_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)

View File

@ -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)

View File

@ -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

View File

@ -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)]

View File

@ -439,7 +439,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
public TestSlider()
{
DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = 0.1f };
SliderVelocity = 0.1f;
DefaultsApplied += _ =>
{

View File

@ -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()
{

View File

@ -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,

View File

@ -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,

View File

@ -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)
}
};

View File

@ -399,7 +399,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
public TestSlider()
{
DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = 0.1f };
SliderVelocity = 0.1f;
DefaultsApplied += _ =>
{

View File

@ -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:

View File

@ -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,
}
}

View File

@ -309,7 +309,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
}
else
{
var result = snapProvider?.FindSnappedPositionAndTime(Parent.ToScreenSpace(e.MousePosition));
var result = snapProvider?.FindSnappedPositionAndTime(Parent.ToScreenSpace(e.MousePosition), SnapType.GlobalGrids);
Vector2 movementDelta = Parent.ToLocalSpace(result?.ScreenSpacePosition ?? Parent.ToScreenSpace(e.MousePosition)) - dragStartPositions[draggedControlPointIndex] - hitObject.Position;

View File

@ -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()
@ -198,7 +198,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
}
// Update the cursor position.
var result = snapProvider?.FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position);
var result = snapProvider?.FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position, state == SliderPlacementState.Body ? SnapType.GlobalGrids : SnapType.All);
cursor.Position = ToLocalSpace(result?.ScreenSpacePosition ?? inputManager.CurrentState.Mouse.Position) - HitObject.Position;
}
else if (cursor != null)

View File

@ -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()
});

View File

@ -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 })

View File

@ -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[]
{
@ -138,7 +143,7 @@ namespace osu.Game.Rulesets.Osu.Edit
// We want to ensure that in this particular case, the time-snapping component of distance snap is still applied.
// The easiest way to ensure this is to attempt application of distance snap after a nearby object is found, and copy over
// the time value if the proposed positions are roughly the same.
if (snapType.HasFlagFast(SnapType.Grids) && DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
if (snapType.HasFlagFast(SnapType.RelativeGrids) && DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
{
(Vector2 distanceSnappedPosition, double distanceSnappedTime) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(snapResult.ScreenSpacePosition));
if (Precision.AlmostEquals(distanceSnapGrid.ToScreenSpace(distanceSnappedPosition), snapResult.ScreenSpacePosition, 1))
@ -150,7 +155,7 @@ namespace osu.Game.Rulesets.Osu.Edit
SnapResult result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
if (snapType.HasFlagFast(SnapType.Grids))
if (snapType.HasFlagFast(SnapType.RelativeGrids))
{
if (DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
{
@ -159,7 +164,10 @@ namespace osu.Game.Rulesets.Osu.Edit
result.ScreenSpacePosition = distanceSnapGrid.ToScreenSpace(pos);
result.Time = time;
}
}
if (snapType.HasFlagFast(SnapType.GlobalGrids))
{
if (rectangularGridSnapToggle.Value == TernaryState.True)
{
Vector2 pos = rectangularPositionSnapGrid.GetSnappedPosition(rectangularPositionSnapGrid.ToLocalSpace(result.ScreenSpacePosition));

View File

@ -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,
};

View File

@ -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 += _ =>

View File

@ -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
}
}

View File

@ -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)

View File

@ -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,

View File

@ -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)

View File

@ -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)

View File

@ -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()

View File

@ -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)

View File

@ -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)

View File

@ -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;
}

View File

@ -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)

View File

@ -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;
}
}

View File

@ -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")
};
}
}

View File

@ -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

View File

@ -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:

View File

@ -10,6 +10,7 @@ namespace osu.Game.Rulesets.Osu
Cursor,
CursorTrail,
CursorParticles,
CursorRipple,
SliderScorePoint,
ReverseArrow,
HitCircleText,

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();

View File

@ -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.Diagnostics;
using System.Linq;
@ -13,6 +11,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Scoring;
using osuTK;
@ -36,8 +35,8 @@ namespace osu.Game.Rulesets.Osu.Statistics
private const float rotation = 45;
private BufferedContainer bufferedGrid;
private GridContainer pointGrid;
private BufferedContainer bufferedGrid = null!;
private GridContainer pointGrid = null!;
private readonly ScoreInfo score;
private readonly IBeatmap playableBeatmap;
@ -58,6 +57,8 @@ namespace osu.Game.Rulesets.Osu.Statistics
[BackgroundDependencyLoader]
private void load()
{
const float line_extension = 0.2f;
InternalChild = new Container
{
Anchor = Anchor.Centre,
@ -66,76 +67,99 @@ namespace osu.Game.Rulesets.Osu.Statistics
FillMode = FillMode.Fit,
Children = new Drawable[]
{
new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(inner_portion),
Masking = true,
BorderThickness = line_thickness,
BorderColour = Color4.White,
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex("#202624")
}
},
new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(inner_portion),
Masking = true,
BorderThickness = line_thickness,
BorderColour = Color4.White,
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex("#202624")
}
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(1),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Rotation = rotation,
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
Children = new Drawable[]
{
new Box
new Circle
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
EdgeSmoothness = new Vector2(1),
RelativeSizeAxes = Axes.Y,
Height = 2, // We're rotating along a diagonal - we don't really care how big this is.
Width = line_thickness / 2,
Rotation = -rotation,
Alpha = 0.3f,
Width = line_thickness,
Height = inner_portion + line_extension,
Rotation = -rotation * 2,
Alpha = 0.6f,
},
new Box
new Circle
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
EdgeSmoothness = new Vector2(1),
RelativeSizeAxes = Axes.Y,
Height = 2, // We're rotating along a diagonal - we don't really care how big this is.
Width = line_thickness / 2, // adjust for edgesmoothness
Rotation = rotation
Width = line_thickness,
Height = inner_portion + line_extension,
},
new OsuSpriteText
{
Text = "Overshoot",
Anchor = Anchor.Centre,
Origin = Anchor.BottomCentre,
Padding = new MarginPadding(3),
RelativePositionAxes = Axes.Both,
Y = -(inner_portion + line_extension) / 2,
},
new OsuSpriteText
{
Text = "Undershoot",
Anchor = Anchor.Centre,
Origin = Anchor.TopCentre,
Padding = new MarginPadding(3),
RelativePositionAxes = Axes.Both,
Y = (inner_portion + line_extension) / 2,
},
new Circle
{
Anchor = Anchor.Centre,
Origin = Anchor.TopCentre,
RelativePositionAxes = Axes.Both,
Y = -(inner_portion + line_extension) / 2,
Margin = new MarginPadding(-line_thickness / 2),
Width = line_thickness,
Height = 10,
Rotation = 45,
},
new Circle
{
Anchor = Anchor.Centre,
Origin = Anchor.TopCentre,
RelativePositionAxes = Axes.Both,
Y = -(inner_portion + line_extension) / 2,
Margin = new MarginPadding(-line_thickness / 2),
Width = line_thickness,
Height = 10,
Rotation = -45,
}
}
},
},
new Box
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Width = 10,
EdgeSmoothness = new Vector2(1),
Height = line_thickness / 2, // adjust for edgesmoothness
},
new Box
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
EdgeSmoothness = new Vector2(1),
Width = line_thickness / 2, // adjust for edgesmoothness
Height = 10,
}
}
},
bufferedGrid = new BufferedContainer(cachedFrameBuffer: true)

View File

@ -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,
}
};
}
}
}
}

View File

@ -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);

View File

@ -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;

View File

@ -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,

View File

@ -0,0 +1,337 @@
// 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.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Timing;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Rulesets.Taiko.UI;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Taiko.Tests
{
public partial class TestSceneDrumSampleTriggerSource : OsuTestScene
{
private readonly ManualClock manualClock = new ManualClock();
[Cached(typeof(IScrollingInfo))]
private ScrollingTestContainer.TestScrollingInfo info = new ScrollingTestContainer.TestScrollingInfo
{
Direction = { Value = ScrollingDirection.Left },
TimeRange = { Value = 200 },
};
private ScrollingHitObjectContainer hitObjectContainer = null!;
private TestDrumSampleTriggerSource triggerSource = null!;
[SetUp]
public void SetUp() => Schedule(() =>
{
hitObjectContainer = new ScrollingHitObjectContainer();
manualClock.CurrentTime = 0;
Child = new Container
{
Clock = new FramedClock(manualClock),
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
hitObjectContainer,
triggerSource = new TestDrumSampleTriggerSource(hitObjectContainer)
}
};
});
[Test]
public void TestNormalHit()
{
AddStep("add hit with normal samples", () =>
{
var hit = new Hit
{
StartTime = 100,
Samples = new List<HitSampleInfo>
{
new HitSampleInfo(HitSampleInfo.HIT_NORMAL)
}
};
hit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
var drawableHit = new DrawableHit(hit);
hitObjectContainer.Add(drawableHit);
});
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
AddStep("seek past hit", () => manualClock.CurrentTime = 200);
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
}
[Test]
public void TestSoftHit()
{
AddStep("add hit with soft samples", () =>
{
var hit = new Hit
{
StartTime = 100,
Samples = new List<HitSampleInfo>
{
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT)
}
};
hit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
var drawableHit = new DrawableHit(hit);
hitObjectContainer.Add(drawableHit);
});
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
AddStep("seek past hit", () => manualClock.CurrentTime = 200);
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
}
[Test]
public void TestDrumStrongHit()
{
AddStep("add strong hit with drum samples", () =>
{
var hit = new Hit
{
StartTime = 100,
Samples = new List<HitSampleInfo>
{
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "drum"),
new HitSampleInfo(HitSampleInfo.HIT_FINISH, "drum") // implies strong
}
};
hit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
var drawableHit = new DrawableHit(hit);
hitObjectContainer.Add(drawableHit);
});
AddAssert("most valid object is strong nested hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit.StrongNestedHit>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
AddStep("seek past hit", () => manualClock.CurrentTime = 200);
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
}
[Test]
public void TestNormalDrumRoll()
{
AddStep("add drum roll with normal samples", () =>
{
var drumRoll = new DrumRoll
{
StartTime = 100,
EndTime = 1100,
Samples = new List<HitSampleInfo>
{
new HitSampleInfo(HitSampleInfo.HIT_NORMAL)
}
};
drumRoll.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
var drawableDrumRoll = new DrawableDrumRoll(drumRoll);
hitObjectContainer.Add(drawableDrumRoll);
});
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
AddStep("seek to middle of drum roll", () => manualClock.CurrentTime = 600);
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
AddStep("seek past drum roll", () => manualClock.CurrentTime = 1200);
AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRoll>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
}
[Test]
public void TestSoftDrumRoll()
{
AddStep("add drum roll with soft samples", () =>
{
var drumRoll = new DrumRoll
{
StartTime = 100,
EndTime = 1100,
Samples = new List<HitSampleInfo>
{
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT)
}
};
drumRoll.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
var drawableDrumRoll = new DrawableDrumRoll(drumRoll);
hitObjectContainer.Add(drawableDrumRoll);
});
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
AddStep("seek to middle of drum roll", () => manualClock.CurrentTime = 600);
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
AddStep("seek past drum roll", () => manualClock.CurrentTime = 1200);
AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRoll>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
}
[Test]
public void TestDrumStrongDrumRoll()
{
AddStep("add strong drum roll with drum samples", () =>
{
var drumRoll = new DrumRoll
{
StartTime = 100,
EndTime = 1100,
Samples = new List<HitSampleInfo>
{
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "drum"),
new HitSampleInfo(HitSampleInfo.HIT_FINISH, "drum") // implies strong
}
};
drumRoll.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
var drawableDrumRoll = new DrawableDrumRoll(drumRoll);
hitObjectContainer.Add(drawableDrumRoll);
});
AddAssert("most valid object is drum roll tick's nested strong hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick.StrongNestedHit>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
AddStep("seek to middle of drum roll", () => manualClock.CurrentTime = 600);
AddAssert("most valid object is drum roll tick's nested strong hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick.StrongNestedHit>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
AddStep("seek past drum roll", () => manualClock.CurrentTime = 1200);
AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRoll>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
}
[Test]
public void TestNormalSwell()
{
AddStep("add swell with normal samples", () =>
{
var swell = new Swell
{
StartTime = 100,
EndTime = 1100,
Samples = new List<HitSampleInfo>
{
new HitSampleInfo(HitSampleInfo.HIT_NORMAL)
}
};
swell.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
var drawableSwell = new DrawableSwell(swell);
hitObjectContainer.Add(drawableSwell);
});
AddAssert("most valid object is swell tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<SwellTick>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
AddStep("seek to middle of swell", () => manualClock.CurrentTime = 600);
AddAssert("most valid object is swell tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<SwellTick>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
AddStep("seek past swell", () => manualClock.CurrentTime = 1200);
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
}
[Test]
public void TestDrumSwell()
{
AddStep("add swell with drum samples", () =>
{
var swell = new Swell
{
StartTime = 100,
EndTime = 1100,
Samples = new List<HitSampleInfo>
{
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "drum")
}
};
swell.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
var drawableSwell = new DrawableSwell(swell);
hitObjectContainer.Add(drawableSwell);
});
AddAssert("most valid object is swell tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<SwellTick>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
AddStep("seek to middle of swell", () => manualClock.CurrentTime = 600);
AddAssert("most valid object is swell tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<SwellTick>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
AddStep("seek past swell", () => manualClock.CurrentTime = 1200);
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
}
private void checkSound(HitType hitType, string expectedName, string expectedBank)
{
AddStep($"hit {hitType}", () => triggerSource.Play(hitType));
AddAssert($"last played sample is {expectedName}", () => triggerSource.LastPlayedSamples!.OfType<HitSampleInfo>().Single().Name, () => Is.EqualTo(expectedName));
AddAssert($"last played sample has {expectedBank} bank", () => triggerSource.LastPlayedSamples!.OfType<HitSampleInfo>().Single().Bank, () => Is.EqualTo(expectedBank));
}
private partial class TestDrumSampleTriggerSource : DrumSampleTriggerSource
{
public ISampleInfo[]? LastPlayedSamples { get; private set; }
public TestDrumSampleTriggerSource(HitObjectContainer hitObjectContainer)
: base(hitObjectContainer)
{
}
protected override void PlaySamples(ISampleInfo[] samples)
{
base.PlaySamples(samples);
LastPlayedSamples = samples;
}
public new HitObject GetMostValidObject() => base.GetMostValidObject();
}
}
}

View File

@ -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;

View File

@ -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)

View File

@ -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;

View File

@ -118,6 +118,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
public override bool RemoveWhenNotAlive => false;
}
// Most osu!taiko hitsounds are managed by the drum (see DrumSampleTriggerSource).
public override IEnumerable<HitSampleInfo> GetSamples() => Enumerable.Empty<HitSampleInfo>();
}
public abstract partial class DrawableTaikoHitObject<TObject> : DrawableTaikoHitObject
@ -157,9 +160,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
Content.Add(MainPiece = CreateMainPiece());
}
// Most osu!taiko hitsounds are managed by the drum (see DrumSampleMapping).
public override IEnumerable<HitSampleInfo> GetSamples() => Enumerable.Empty<HitSampleInfo>();
protected abstract SkinnableDrawable CreateMainPiece();
}
}

View File

@ -5,6 +5,7 @@
using osu.Game.Rulesets.Objects.Types;
using System.Threading;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats;
@ -15,7 +16,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 +36,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 +66,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 +95,8 @@ namespace osu.Game.Rulesets.Taiko.Objects
FirstTick = first,
TickSpacing = tickSpacing,
StartTime = t,
IsStrong = IsStrong
IsStrong = IsStrong,
Samples = Samples
});
first = false;
@ -92,7 +107,11 @@ namespace osu.Game.Rulesets.Taiko.Objects
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit { StartTime = startTime };
protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit
{
StartTime = startTime,
Samples = Samples
};
public class StrongNestedHit : StrongNestedHitObject
{

View File

@ -33,7 +33,11 @@ namespace osu.Game.Rulesets.Taiko.Objects
public override double MaximumJudgementOffset => HitWindow;
protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit { StartTime = startTime };
protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit
{
StartTime = startTime,
Samples = Samples
};
public class StrongNestedHit : StrongNestedHitObject
{

View File

@ -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)
@ -72,7 +72,11 @@ namespace osu.Game.Rulesets.Taiko.Objects
}
}
protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit { StartTime = startTime };
protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit
{
StartTime = startTime,
Samples = Samples
};
public class StrongNestedHit : StrongNestedHitObject
{

View File

@ -33,7 +33,10 @@ namespace osu.Game.Rulesets.Taiko.Objects
for (int i = 0; i < RequiredHits; i++)
{
cancellationToken.ThrowIfCancellationRequested();
AddNested(new SwellTick());
AddNested(new SwellTick
{
Samples = Samples
});
}
}

View File

@ -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)

View File

@ -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);

View File

@ -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");

View File

@ -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;
}

View File

@ -253,17 +253,17 @@ namespace osu.Game.Tests.Beatmaps.Formats
var soundPoint = controlPoints.SamplePointAt(0);
Assert.AreEqual(956, soundPoint.Time);
Assert.AreEqual("soft", soundPoint.SampleBank);
Assert.AreEqual(HitSampleInfo.BANK_SOFT, soundPoint.SampleBank);
Assert.AreEqual(60, soundPoint.SampleVolume);
soundPoint = controlPoints.SamplePointAt(53373);
Assert.AreEqual(53373, soundPoint.Time);
Assert.AreEqual("soft", soundPoint.SampleBank);
Assert.AreEqual(HitSampleInfo.BANK_SOFT, soundPoint.SampleBank);
Assert.AreEqual(60, soundPoint.SampleVolume);
soundPoint = controlPoints.SamplePointAt(119637);
Assert.AreEqual(119637, soundPoint.Time);
Assert.AreEqual("soft", soundPoint.SampleBank);
Assert.AreEqual(HitSampleInfo.BANK_SOFT, soundPoint.SampleBank);
Assert.AreEqual(80, soundPoint.SampleVolume);
var effectPoint = controlPoints.EffectPointAt(0);
@ -305,10 +305,10 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.That(controlPoints.EffectPointAt(2500).KiaiMode, Is.False);
Assert.That(controlPoints.EffectPointAt(3500).KiaiMode, Is.True);
Assert.That(controlPoints.SamplePointAt(500).SampleBank, Is.EqualTo("drum"));
Assert.That(controlPoints.SamplePointAt(1500).SampleBank, Is.EqualTo("drum"));
Assert.That(controlPoints.SamplePointAt(2500).SampleBank, Is.EqualTo("normal"));
Assert.That(controlPoints.SamplePointAt(3500).SampleBank, Is.EqualTo("drum"));
Assert.That(controlPoints.SamplePointAt(500).SampleBank, Is.EqualTo(HitSampleInfo.BANK_DRUM));
Assert.That(controlPoints.SamplePointAt(1500).SampleBank, Is.EqualTo(HitSampleInfo.BANK_DRUM));
Assert.That(controlPoints.SamplePointAt(2500).SampleBank, Is.EqualTo(HitSampleInfo.BANK_NORMAL));
Assert.That(controlPoints.SamplePointAt(3500).SampleBank, Is.EqualTo(HitSampleInfo.BANK_DRUM));
Assert.That(controlPoints.TimingPointAt(500).BeatLength, Is.EqualTo(500).Within(0.1));
Assert.That(controlPoints.TimingPointAt(1500).BeatLength, Is.EqualTo(500).Within(0.1));
@ -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]

View File

@ -95,6 +95,27 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
[Test]
public void TestLoopWithoutExplicitFadeOut()
{
var decoder = new LegacyStoryboardDecoder();
using (var resStream = TestResources.OpenResource("animation-loop-no-explicit-end-time.osb"))
using (var stream = new LineBufferedReader(resStream))
{
var storyboard = decoder.Decode(stream);
StoryboardLayer background = storyboard.Layers.Single(l => l.Depth == 3);
Assert.AreEqual(1, background.Elements.Count);
Assert.AreEqual(2000, background.Elements[0].StartTime);
Assert.AreEqual(2000, (background.Elements[0] as StoryboardAnimation)?.EarliestTransformTime);
Assert.AreEqual(3000, (background.Elements[0] as StoryboardAnimation)?.GetEndTime());
Assert.AreEqual(12000, (background.Elements[0] as StoryboardAnimation)?.EndTimeForDisplay);
}
}
[Test]
public void TestCorrectAnimationStartTime()
{

View File

@ -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;
}
}
}

View File

@ -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);

View File

@ -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);

View File

@ -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));
}
}
}

View File

@ -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; }
}
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -0,0 +1,6 @@
[Events]
//Storyboard Layer 0 (Background)
Animation,Background,Centre,"img.jpg",320,240,2,150,LoopForever
F,0,2000,,0,1
L,2000,10
F,18,0,1000,1,0

View File

@ -0,0 +1,31 @@
osu file format v14
[Events]
//Background and Video events
0,0,"BG.jpg",0,0
Video,0,"video.avi"
//Break Periods
//Storyboard Layer 0 (Background)
//Storyboard Layer 1 (Fail)
//Storyboard Layer 2 (Pass)
//Storyboard Layer 3 (Foreground)
//Storyboard Layer 4 (Overlay)
//Storyboard Sound Samples
[TimingPoints]
1674,333.333333333333,4,2,1,70,1,0
1674,-100,4,2,1,70,0,0
3340,-100,4,2,1,70,0,0
3507,-100,4,2,1,70,0,0
3673,-100,4,2,1,70,0,0
[Colours]
Combo1 : 240,80,80
Combo2 : 171,252,203
Combo3 : 128,128,255
Combo4 : 249,254,186
[HitObjects]
148,303,1674,5,6,3:2:0:0:
378,252,1840,1,0,0:0:0:0:
389,270,2340,5,2,0:1:0:0:

View File

@ -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

View File

@ -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");
});
}

View File

@ -311,6 +311,7 @@ namespace osu.Game.Tests.Visual.Background
public bool IsDrawable => true;
public double StartTime => double.MinValue;
public double EndTime => double.MaxValue;
public double EndTimeForDisplay => double.MaxValue;
public Drawable CreateDrawable() => new DrawableTestStoryboardElement();
}

View File

@ -42,7 +42,8 @@ namespace osu.Game.Tests.Visual.Editing
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(90, 90)
Size = new Vector2(90, 90),
Scale = new Vector2(3),
}
};
});
@ -64,17 +65,24 @@ namespace osu.Game.Tests.Visual.Editing
InputManager.MoveMouseTo(tickMarkerHead.ScreenSpaceDrawQuad.Centre);
InputManager.PressButton(MouseButton.Left);
});
AddStep("move to 8 and release", () =>
AddStep("move to 1", () => InputManager.MoveMouseTo(getPositionForDivisor(1)));
AddStep("move to 16 and release", () =>
{
InputManager.MoveMouseTo(tickSliderBar.ScreenSpaceDrawQuad.Centre);
InputManager.MoveMouseTo(getPositionForDivisor(16));
InputManager.ReleaseButton(MouseButton.Left);
});
AddAssert("divisor is 8", () => bindableBeatDivisor.Value == 8);
AddAssert("divisor is 16", () => bindableBeatDivisor.Value == 16);
AddStep("hold marker", () => InputManager.PressButton(MouseButton.Left));
AddStep("move to 16", () => InputManager.MoveMouseTo(getPositionForDivisor(16)));
AddStep("move to ~10 and release", () =>
AddStep("move to ~6 and release", () =>
{
InputManager.MoveMouseTo(getPositionForDivisor(6));
InputManager.ReleaseButton(MouseButton.Left);
});
AddAssert("divisor clamped to 8", () => bindableBeatDivisor.Value == 8);
AddStep("move to ~10 and click", () =>
{
InputManager.MoveMouseTo(getPositionForDivisor(10));
InputManager.PressButton(MouseButton.Left);
InputManager.ReleaseButton(MouseButton.Left);
});
AddAssert("divisor clamped to 8", () => bindableBeatDivisor.Value == 8);
@ -82,12 +90,11 @@ namespace osu.Game.Tests.Visual.Editing
private Vector2 getPositionForDivisor(int divisor)
{
float relativePosition = (float)Math.Clamp(divisor, 0, 16) / 16;
var sliderDrawQuad = tickSliderBar.ScreenSpaceDrawQuad;
return new Vector2(
sliderDrawQuad.TopLeft.X + sliderDrawQuad.Width * relativePosition,
sliderDrawQuad.Centre.Y
);
float localX = (1 - 1 / (float)divisor) * tickSliderBar.UsableWidth + tickSliderBar.RangePadding;
return tickSliderBar.ToScreenSpace(new Vector2(
localX,
tickSliderBar.DrawHeight / 2
));
}
[Test]

View File

@ -187,7 +187,7 @@ namespace osu.Game.Tests.Visual.Editing
private class SnapProvider : IDistanceSnapProvider
{
public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.Grids) => new SnapResult(screenSpacePosition, 0);
public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.AllGrids) => new SnapResult(screenSpacePosition, 0);
public Bindable<double> DistanceSpacingMultiplier { get; } = new BindableDouble(1);

View File

@ -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]

View File

@ -1,57 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Extensions;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.GameplayTest;
using osu.Game.Screens.Select;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.Editing
{
public partial class TestSceneEditorNavigation : OsuGameTestScene
{
[Test]
public void TestEditorGameplayTestAlwaysUsesOriginalRuleset()
{
BeatmapSetInfo beatmapSet = null!;
AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely());
AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach());
AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet));
AddUntilStep("wait for song select",
() => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
&& Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
&& songSelect.IsLoaded);
AddStep("switch ruleset", () => Game.Ruleset.Value = new ManiaRuleset().RulesetInfo);
AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0)));
AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
AddStep("test gameplay", () => ((Editor)Game.ScreenStack.CurrentScreen).TestGameplay());
AddUntilStep("wait for player", () =>
{
// notifications may fire at almost any inopportune time and cause annoying test failures.
// relentlessly attempt to dismiss any and all interfering overlays, which includes notifications.
// this is theoretically not foolproof, but it's the best that can be done here.
Game.CloseAllOverlays();
return Game.ScreenStack.CurrentScreen is EditorPlayer editorPlayer && editorPlayer.IsLoaded;
});
AddAssert("current ruleset is osu!", () => Game.Ruleset.Value.Equals(new OsuRuleset().RulesetInfo));
AddStep("exit to song select", () => Game.PerformFromScreen(_ => { }, typeof(PlaySongSelect).Yield()));
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect);
AddAssert("previous ruleset restored", () => Game.Ruleset.Value.Equals(new ManiaRuleset().RulesetInfo));
}
}
}

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