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

Merge branch 'master' into bspline-sliders

This commit is contained in:
Bartłomiej Dach 2023-11-20 12:03:29 +09:00
commit b6e3e42445
No known key found for this signature in database
122 changed files with 2268 additions and 400 deletions

View File

@ -21,7 +21,7 @@
]
},
"ppy.localisationanalyser.tools": {
"version": "2023.712.0",
"version": "2023.1117.0",
"commands": [
"localisation"
]

View File

@ -185,9 +185,11 @@ jobs:
- name: Add comment environment
if: ${{ github.event_name == 'issue_comment' }}
env:
COMMENT_BODY: ${{ github.event.comment.body }}
run: |
# Add comment environment
echo '${{ github.event.comment.body }}' | sed -r 's/\r$//' | grep -E '^\w+=' | while read -r line; do
echo $COMMENT_BODY | sed -r 's/\r$//' | grep -E '^\w+=' | while read -r line; do
opt=$(echo ${line} | cut -d '=' -f1)
sed -i "s;^${opt}=.*$;${line};" "${{ needs.directory.outputs.GENERATOR_ENV }}"
done

View File

@ -70,7 +70,7 @@ namespace osu.Android
},
new SettingsCheckbox
{
LabelText = MouseSettingsStrings.DisableMouseButtons,
LabelText = MouseSettingsStrings.DisableClicksDuringGameplay,
Current = osuConfig.GetBindable<bool>(OsuSetting.MouseDisableButtons),
},
});

View File

@ -11,6 +11,7 @@ using osu.Framework.Input.Handlers;
using osu.Framework.Platform;
using osu.Game;
using osu.Game.Overlays.Settings;
using osu.Game.Overlays.Settings.Sections.Input;
using osu.Game.Updater;
using osu.Game.Utils;
@ -97,6 +98,9 @@ namespace osu.Android
case AndroidJoystickHandler jh:
return new AndroidJoystickSettings(jh);
case AndroidTouchHandler th:
return new TouchSettings(th);
default:
return base.CreateSettingsSubsectionFor(handler);
}

View File

@ -10,8 +10,8 @@ using osu.Game;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Screens.Play;
using Squirrel;
using Squirrel.SimpleSplat;
using Squirrel.Sources;
using LogLevel = Squirrel.SimpleSplat.LogLevel;
using UpdateManager = osu.Game.Updater.UpdateManager;
@ -63,7 +63,7 @@ namespace osu.Desktop.Updater
if (localUserInfo?.IsPlaying.Value == true)
return false;
updateManager ??= new GithubUpdateManager(@"https://github.com/ppy/osu", false, github_token, @"osulazer");
updateManager ??= new Squirrel.UpdateManager(new GithubSource(@"https://github.com/ppy/osu", github_token, false), @"osulazer");
var info = await updateManager.CheckForUpdate(!useDeltaPatching).ConfigureAwait(false);

View File

@ -23,7 +23,7 @@
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Clowd.Squirrel" Version="2.9.42" />
<PackageReference Include="Clowd.Squirrel" Version="2.10.2" />
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
<PackageReference Include="System.IO.Packaging" Version="7.0.0" />
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />

View File

@ -28,9 +28,9 @@ namespace osu.Game.Rulesets.Catch.Tests
private class TestLegacySkin : LegacySkin
{
public TestLegacySkin(SkinInfo skin, IResourceStore<byte[]> storage)
public TestLegacySkin(SkinInfo skin, IResourceStore<byte[]> fallbackStore)
// Bypass LegacySkinResourceStore to avoid returning null for retrieving files due to bad skin info (SkinInfo.Files = null).
: base(skin, null, storage)
: base(skin, null, fallbackStore)
{
}
}

View File

@ -0,0 +1,204 @@
// 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.Allocation;
using osu.Framework.Input;
using osu.Framework.Screens;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Timing;
using osu.Game.Configuration;
using osu.Game.Input;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
using osu.Game.Storyboards;
using osu.Game.Tests.Visual;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public partial class TestSceneOsuModTouchDevice : RateAdjustedBeatmapTestScene
{
[Resolved]
private SessionStatics statics { get; set; } = null!;
private ScoreAccessibleSoloPlayer currentPlayer = null!;
private readonly ManualClock manualClock = new ManualClock { Rate = 0 };
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null)
=> new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(manualClock), Audio);
[BackgroundDependencyLoader]
private void load()
{
Add(new TouchInputInterceptor());
}
public override void SetUpSteps()
{
AddStep("reset static", () => statics.SetValue(Static.TouchInputActive, false));
base.SetUpSteps();
}
[Test]
public void TestUserAlreadyHasTouchDeviceActive()
{
loadPlayer();
// it is presumed that a previous screen (i.e. song select) will set this up
AddStep("set up touchscreen user", () =>
{
currentPlayer.Score.ScoreInfo.Mods = currentPlayer.Score.ScoreInfo.Mods.Append(new OsuModTouchDevice()).ToArray();
statics.SetValue(Static.TouchInputActive, true);
});
AddStep("seek to 0", () => currentPlayer.GameplayClockContainer.Seek(0));
AddUntilStep("wait until 0", () => currentPlayer.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(0));
AddStep("touch circle", () =>
{
var touch = new Touch(TouchSource.Touch1, currentPlayer.DrawableRuleset.Playfield.ScreenSpaceDrawQuad.Centre);
InputManager.BeginTouch(touch);
InputManager.EndTouch(touch);
});
AddAssert("touch device mod activated", () => currentPlayer.Score.ScoreInfo.Mods, () => Has.One.InstanceOf<OsuModTouchDevice>());
}
[Test]
public void TestTouchDuringBreak()
{
loadPlayer();
AddStep("seek to 2000", () => currentPlayer.GameplayClockContainer.Seek(2000));
AddUntilStep("wait until 2000", () => currentPlayer.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(2000));
AddUntilStep("wait until break entered", () => currentPlayer.IsBreakTime.Value);
AddStep("touch playfield", () =>
{
var touch = new Touch(TouchSource.Touch1, currentPlayer.DrawableRuleset.Playfield.ScreenSpaceDrawQuad.Centre);
InputManager.BeginTouch(touch);
InputManager.EndTouch(touch);
});
AddAssert("touch device mod not activated", () => currentPlayer.Score.ScoreInfo.Mods, () => Has.None.InstanceOf<OsuModTouchDevice>());
}
[Test]
public void TestTouchMiss()
{
loadPlayer();
// ensure mouse is active (and that it's not suppressed due to touches in previous tests)
AddStep("click mouse", () => InputManager.Click(MouseButton.Left));
AddStep("seek to 200", () => currentPlayer.GameplayClockContainer.Seek(200));
AddUntilStep("wait until 200", () => currentPlayer.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(200));
AddStep("touch playfield", () =>
{
var touch = new Touch(TouchSource.Touch1, currentPlayer.DrawableRuleset.Playfield.ScreenSpaceDrawQuad.Centre);
InputManager.BeginTouch(touch);
InputManager.EndTouch(touch);
});
AddAssert("touch device mod activated", () => currentPlayer.Score.ScoreInfo.Mods, () => Has.One.InstanceOf<OsuModTouchDevice>());
}
[Test]
public void TestIncompatibleModActive()
{
loadPlayer();
// this is only a veneer of enabling autopilot as having it actually active from the start is annoying to make happen
// given the tests' structure.
AddStep("enable autopilot", () => currentPlayer.Score.ScoreInfo.Mods = new Mod[] { new OsuModAutopilot() });
AddStep("seek to 0", () => currentPlayer.GameplayClockContainer.Seek(0));
AddUntilStep("wait until 0", () => currentPlayer.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(0));
AddStep("touch playfield", () =>
{
var touch = new Touch(TouchSource.Touch1, currentPlayer.DrawableRuleset.Playfield.ScreenSpaceDrawQuad.Centre);
InputManager.BeginTouch(touch);
InputManager.EndTouch(touch);
});
AddAssert("touch device mod not activated", () => currentPlayer.Score.ScoreInfo.Mods, () => Has.None.InstanceOf<OsuModTouchDevice>());
}
[Test]
public void TestSecondObjectTouched()
{
loadPlayer();
// ensure mouse is active (and that it's not suppressed due to touches in previous tests)
AddStep("click mouse", () => InputManager.Click(MouseButton.Left));
AddStep("seek to 0", () => currentPlayer.GameplayClockContainer.Seek(0));
AddUntilStep("wait until 0", () => currentPlayer.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(0));
AddStep("click circle", () =>
{
InputManager.MoveMouseTo(currentPlayer.DrawableRuleset.Playfield.ScreenSpaceDrawQuad.Centre);
InputManager.Click(MouseButton.Left);
});
AddAssert("touch device mod not activated", () => currentPlayer.Score.ScoreInfo.Mods, () => Has.None.InstanceOf<OsuModTouchDevice>());
AddStep("seek to 5000", () => currentPlayer.GameplayClockContainer.Seek(5000));
AddUntilStep("wait until 5000", () => currentPlayer.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(5000));
AddStep("touch playfield", () =>
{
var touch = new Touch(TouchSource.Touch1, currentPlayer.DrawableRuleset.Playfield.ScreenSpaceDrawQuad.Centre);
InputManager.BeginTouch(touch);
InputManager.EndTouch(touch);
});
AddAssert("touch device mod activated", () => currentPlayer.Score.ScoreInfo.Mods, () => Has.One.InstanceOf<OsuModTouchDevice>());
}
private void loadPlayer()
{
AddStep("load player", () =>
{
Beatmap.Value = CreateWorkingBeatmap(new OsuBeatmap
{
HitObjects =
{
new HitCircle
{
Position = OsuPlayfield.BASE_SIZE / 2,
StartTime = 0,
},
new HitCircle
{
Position = OsuPlayfield.BASE_SIZE / 2,
StartTime = 5000,
},
},
Breaks =
{
new BreakPeriod(2000, 3000)
}
});
var p = new ScoreAccessibleSoloPlayer();
LoadScreen(currentPlayer = p);
});
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
}
private partial class ScoreAccessibleSoloPlayer : SoloPlayer
{
public new GameplayClockContainer GameplayClockContainer => base.GameplayClockContainer;
public new DrawableRuleset DrawableRuleset => base.DrawableRuleset;
protected override bool PauseOnFocusLost => false;
public ScoreAccessibleSoloPlayer()
: base(new PlayerConfiguration
{
AllowPause = false,
ShowResults = false,
})
{
}
}
}
}

View File

@ -15,22 +15,22 @@ namespace osu.Game.Rulesets.Osu.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
[TestCase(6.710442985146793d, 206, "diffcalc-test")]
[TestCase(1.4386882251130073d, 45, "zero-length-sliders")]
[TestCase(0.42506480230838789d, 2, "very-fast-slider")]
[TestCase(0.14102693012101306d, 1, "nan-slider")]
[TestCase(6.710442985146793d, 239, "diffcalc-test")]
[TestCase(1.4386882251130073d, 54, "zero-length-sliders")]
[TestCase(0.42506480230838789d, 4, "very-fast-slider")]
[TestCase(0.14102693012101306d, 2, "nan-slider")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name);
[TestCase(8.9742952703071666d, 206, "diffcalc-test")]
[TestCase(0.55071082800473514d, 2, "very-fast-slider")]
[TestCase(1.743180218215227d, 45, "zero-length-sliders")]
[TestCase(8.9742952703071666d, 239, "diffcalc-test")]
[TestCase(1.743180218215227d, 54, "zero-length-sliders")]
[TestCase(0.55071082800473514d, 4, "very-fast-slider")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
[TestCase(6.710442985146793d, 239, "diffcalc-test")]
[TestCase(0.42506480230838789d, 4, "very-fast-slider")]
[TestCase(1.4386882251130073d, 54, "zero-length-sliders")]
[TestCase(0.42506480230838789d, 4, "very-fast-slider")]
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic());

View File

@ -133,8 +133,11 @@ namespace osu.Game.Rulesets.Osu.Tests
}
[Test]
public void TestSimpleInput()
public void TestSimpleInput([Values] bool disableMouseButtons)
{
// OsuSetting.MouseDisableButtons should not affect touch taps
AddStep($"{(disableMouseButtons ? "disable" : "enable")} mouse buttons", () => config.SetValue(OsuSetting.MouseDisableButtons, disableMouseButtons));
beginTouch(TouchSource.Touch1);
assertKeyCounter(1, 0);
@ -468,7 +471,7 @@ namespace osu.Game.Rulesets.Osu.Tests
[Test]
public void TestInputWhileMouseButtonsDisabled()
{
AddStep("Disable mouse buttons", () => config.SetValue(OsuSetting.MouseDisableButtons, true));
AddStep("Disable gameplay taps", () => config.SetValue(OsuSetting.TouchDisableGameplayTaps, true));
beginTouch(TouchSource.Touch1);
@ -620,6 +623,7 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep("Release all touches", () =>
{
config.SetValue(OsuSetting.MouseDisableButtons, false);
config.SetValue(OsuSetting.TouchDisableGameplayTaps, false);
foreach (TouchSource source in InputManager.CurrentState.Touch.ActiveSources)
InputManager.EndTouch(new Touch(source, osuInputManager.ScreenSpaceDrawQuad.Centre));
});

View File

@ -157,7 +157,7 @@ namespace osu.Game.Rulesets.Osu.Tests
if (hit)
assertAllMaxJudgements();
else
AddAssert("Tracking dropped", assertMidSliderJudgementFail);
assertMidSliderJudgementFail();
AddAssert("Head judgement is first", () => judgementResults.First().HitObject is SliderHeadCircle);
@ -197,7 +197,7 @@ namespace osu.Game.Rulesets.Osu.Tests
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_during_slide_1 },
});
AddAssert("Tracking lost", assertMidSliderJudgementFail);
assertMidSliderJudgementFail();
}
/// <summary>
@ -278,7 +278,7 @@ namespace osu.Game.Rulesets.Osu.Tests
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_before_slider },
});
AddAssert("Tracking retained, sliderhead miss", assertHeadMissTailTracked);
assertHeadMissTailTracked();
}
/// <summary>
@ -302,7 +302,7 @@ namespace osu.Game.Rulesets.Osu.Tests
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_during_slide_4 },
});
AddAssert("Tracking re-acquired", assertMidSliderJudgements);
assertMidSliderJudgements();
}
/// <summary>
@ -328,7 +328,7 @@ namespace osu.Game.Rulesets.Osu.Tests
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_during_slide_4 },
});
AddAssert("Tracking lost", assertMidSliderJudgementFail);
assertMidSliderJudgementFail();
}
/// <summary>
@ -350,7 +350,7 @@ namespace osu.Game.Rulesets.Osu.Tests
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_during_slide_4 },
});
AddAssert("Tracking acquired", assertMidSliderJudgements);
assertMidSliderJudgements();
}
/// <summary>
@ -373,7 +373,7 @@ namespace osu.Game.Rulesets.Osu.Tests
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_during_slide_2 },
});
AddAssert("Tracking acquired", assertMidSliderJudgements);
assertMidSliderJudgements();
}
[Test]
@ -387,7 +387,7 @@ namespace osu.Game.Rulesets.Osu.Tests
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.RightButton }, Time = time_during_slide_2 },
});
AddAssert("Tracking acquired", assertMidSliderJudgements);
assertMidSliderJudgements();
}
/// <summary>
@ -412,7 +412,7 @@ namespace osu.Game.Rulesets.Osu.Tests
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_during_slide_4 },
});
AddAssert("Tracking acquired", assertMidSliderJudgements);
assertMidSliderJudgements();
}
/// <summary>
@ -454,7 +454,7 @@ namespace osu.Game.Rulesets.Osu.Tests
new OsuReplayFrame { Position = new Vector2(slider_path_length, OsuHitObject.OBJECT_RADIUS * 1.201f), Actions = { OsuAction.LeftButton }, Time = time_slider_end },
});
AddAssert("Tracking dropped", assertMidSliderJudgementFail);
assertMidSliderJudgementFail();
}
private void assertAllMaxJudgements()
@ -465,11 +465,21 @@ namespace osu.Game.Rulesets.Osu.Tests
}, () => Is.EqualTo(judgementResults.Select(j => (j.HitObject, j.Judgement.MaxResult))));
}
private bool assertHeadMissTailTracked() => judgementResults[^2].Type == HitResult.SmallTickHit && !judgementResults.First().IsHit;
private void assertHeadMissTailTracked()
{
AddAssert("Tracking retained", () => judgementResults[^2].Type, () => Is.EqualTo(HitResult.LargeTickHit));
AddAssert("Slider head missed", () => judgementResults.First().IsHit, () => Is.False);
}
private bool assertMidSliderJudgements() => judgementResults[^2].Type == HitResult.SmallTickHit;
private void assertMidSliderJudgements()
{
AddAssert("Tracking acquired", () => judgementResults[^2].Type, () => Is.EqualTo(HitResult.LargeTickHit));
}
private bool assertMidSliderJudgementFail() => judgementResults[^2].Type == HitResult.SmallTickMiss;
private void assertMidSliderJudgementFail()
{
AddAssert("Tracking lost", () => judgementResults[^2].Type, () => Is.EqualTo(HitResult.IgnoreMiss));
}
private void performTest(List<ReplayFrame> frames, Slider? slider = null, double? bpm = null, int? tickRate = null)
{

View File

@ -3,6 +3,7 @@
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
@ -36,6 +37,12 @@ namespace osu.Game.Rulesets.Osu.Tests
AddSliderStep("Spin rate", 0.5, 5, 1, val => spinRate.Value = val);
}
[SetUpSteps]
public void SetUpSteps()
{
AddStep("Reset rate", () => spinRate.Value = 1);
}
[TestCase(true)]
[TestCase(false)]
public void TestVariousSpinners(bool autoplay)
@ -46,6 +53,36 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep($"{term} Small", () => SetContents(_ => testSingle(7, autoplay)));
}
[Test]
public void TestSpinnerNoBonus()
{
AddStep("Set high spin rate", () => spinRate.Value = 5);
Spinner spinner;
AddStep("add spinner", () => SetContents(_ =>
{
spinner = new Spinner
{
StartTime = Time.Current,
EndTime = Time.Current + 750,
Samples = new List<HitSampleInfo>
{
new HitSampleInfo(HitSampleInfo.HIT_NORMAL)
}
};
spinner.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { OverallDifficulty = 0 });
return drawableSpinner = new TestDrawableSpinner(spinner, true, spinRate)
{
Anchor = Anchor.Centre,
Depth = depthIndex++,
Scale = new Vector2(0.75f)
};
}));
}
[Test]
public void TestSpinningSamplePitchShift()
{
@ -153,7 +190,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
base.Update();
if (auto)
RotationTracker.AddRotation((float)(Clock.ElapsedFrameTime * spinRate.Value));
RotationTracker.AddRotation((float)Math.Min(180, Clock.ElapsedFrameTime * spinRate.Value));
}
}
}

View File

@ -42,6 +42,7 @@ namespace osu.Game.Rulesets.Osu.Edit
SelectionBox.CanFlipX = SelectionBox.CanScaleX = quad.Width > 0;
SelectionBox.CanFlipY = SelectionBox.CanScaleY = quad.Height > 0;
SelectionBox.CanScaleDiagonally = SelectionBox.CanScaleX && SelectionBox.CanScaleY;
SelectionBox.CanReverse = EditorBeatmap.SelectedHitObjects.Count > 1 || EditorBeatmap.SelectedHitObjects.Any(s => s is Slider);
}

View File

@ -33,7 +33,8 @@ namespace osu.Game.Rulesets.Osu.Mods
typeof(ModNoFail),
typeof(ModAutoplay),
typeof(OsuModMagnetised),
typeof(OsuModRepel)
typeof(OsuModRepel),
typeof(ModTouchDevice)
};
public bool PerformFail() => false;

View File

@ -1,18 +1,14 @@
// 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.Localisation;
using System;
using System.Linq;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModTouchDevice : Mod
public class OsuModTouchDevice : ModTouchDevice
{
public override string Name => "Touch Device";
public override string Acronym => "TD";
public override LocalisableString Description => "Automatically applied to plays on devices with a touchscreen.";
public override double ScoreMultiplier => 1;
public override ModType Type => ModType.System;
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot) }).ToArray();
}
}

View File

@ -4,6 +4,7 @@
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
@ -45,6 +46,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private const float spinning_sample_initial_frequency = 1.0f;
private const float spinning_sample_modulated_base_frequency = 0.5f;
private SkinnableSound maxBonusSample;
/// <summary>
/// The amount of bonus score gained from spinning after the required number of spins, for display purposes.
/// </summary>
@ -109,6 +112,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
MinimumSampleVolume = MINIMUM_SAMPLE_VOLUME,
Looping = true,
Frequency = { Value = spinning_sample_initial_frequency }
},
maxBonusSample = new SkinnableSound
{
MinimumSampleVolume = MINIMUM_SAMPLE_VOLUME,
}
});
@ -128,6 +135,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
base.OnFree();
spinningSample.ClearSamples();
maxBonusSample.ClearSamples();
}
protected override void LoadSamples()
@ -136,6 +144,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
spinningSample.Samples = HitObject.CreateSpinningSamples().Cast<ISampleInfo>().ToArray();
spinningSample.Frequency.Value = spinning_sample_initial_frequency;
maxBonusSample.Samples = new ISampleInfo[] { new SpinnerBonusMaxSampleInfo(HitObject.CreateHitSampleInfo()) };
}
private void updateSpinningSample(ValueChangedEvent<bool> tracking)
@ -157,6 +167,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
base.StopAllSamples();
spinningSample?.Stop();
maxBonusSample?.Stop();
}
protected override void AddNestedHitObject(DrawableHitObject hitObject)
@ -322,10 +333,38 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
var tick = ticks.FirstOrDefault(t => !t.Result.HasResult);
// tick may be null if we've hit the spin limit.
tick?.TriggerResult(true);
if (tick == null)
{
// we still want to play a sound. this will probably be a new sound in the future, but for now let's continue playing the bonus sound.
// TODO: this doesn't concurrency. i can't figure out how to make it concurrency. samples are bad and need a refactor.
maxBonusSample.Play();
}
else
tick.TriggerResult(true);
completedFullSpins.Value++;
}
}
public class SpinnerBonusMaxSampleInfo : HitSampleInfo
{
public override IEnumerable<string> LookupNames
{
get
{
foreach (string name in base.LookupNames)
yield return name;
foreach (string name in base.LookupNames)
yield return name.Replace("-max", string.Empty);
}
}
public SpinnerBonusMaxSampleInfo(HitSampleInfo sampleInfo)
: base("spinnerbonus-max", sampleInfo.Bank, sampleInfo.Suffix, sampleInfo.Volume)
{
}
}
}
}

View File

@ -135,6 +135,8 @@ namespace osu.Game.Rulesets.Osu.Objects
classicSliderBehaviour = value;
if (HeadCircle != null)
HeadCircle.ClassicSliderBehaviour = value;
if (TailCircle != null)
TailCircle.ClassicSliderBehaviour = value;
}
}
@ -218,6 +220,7 @@ namespace osu.Game.Rulesets.Osu.Objects
StartTime = e.Time,
Position = EndPosition,
StackHeight = StackHeight,
ClassicSliderBehaviour = ClassicSliderBehaviour,
});
break;
@ -273,9 +276,9 @@ namespace osu.Game.Rulesets.Osu.Objects
}
public override Judgement CreateJudgement() => ClassicSliderBehaviour
// See logic in `DrawableSlider.CheckForResult()`
// Final combo is provided by the slider itself - see logic in `DrawableSlider.CheckForResult()`
? new OsuJudgement()
// Of note, this creates a combo discrepancy for non-classic-mod sliders (there is no combo increase for tail or slider judgement).
// Final combo is provided by the tail circle - see `SliderTailCircle`
: new OsuIgnoreJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty;

View File

@ -3,6 +3,8 @@
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects
@ -43,5 +45,12 @@ namespace osu.Game.Rulesets.Osu.Objects
}
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
public override Judgement CreateJudgement() => new SliderEndJudgement();
public class SliderEndJudgement : OsuJudgement
{
public override HitResult MaxResult => HitResult.LargeTickHit;
}
}
}

View File

@ -1,10 +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.
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects
{
public class SliderRepeat : SliderEndCircle
@ -13,12 +9,5 @@ namespace osu.Game.Rulesets.Osu.Objects
: base(slider)
{
}
public override Judgement CreateJudgement() => new SliderRepeatJudgement();
public class SliderRepeatJudgement : OsuJudgement
{
public override HitResult MaxResult => HitResult.LargeTickHit;
}
}
}

View File

@ -9,16 +9,28 @@ namespace osu.Game.Rulesets.Osu.Objects
{
public class SliderTailCircle : SliderEndCircle
{
/// <summary>
/// Whether to treat this <see cref="SliderHeadCircle"/> as a normal <see cref="HitCircle"/> for judgement purposes.
/// If <c>false</c>, this <see cref="SliderHeadCircle"/> will be judged as a <see cref="SliderTick"/> instead.
/// </summary>
public bool ClassicSliderBehaviour;
public SliderTailCircle(Slider slider)
: base(slider)
{
}
public override Judgement CreateJudgement() => new SliderTailJudgement();
public override Judgement CreateJudgement() => ClassicSliderBehaviour ? new LegacyTailJudgement() : new TailJudgement();
public class SliderTailJudgement : OsuJudgement
public class LegacyTailJudgement : OsuJudgement
{
public override HitResult MaxResult => HitResult.SmallTickHit;
}
public class TailJudgement : SliderEndJudgement
{
public override HitResult MaxResult => HitResult.LargeTickHit;
public override HitResult MinResult => HitResult.IgnoreMiss;
}
}
}

View File

@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.UI
private readonly OsuInputManager osuInputManager;
private Bindable<bool> mouseDisabled = null!;
private Bindable<bool> tapsDisabled = null!;
public OsuTouchInputMapper(OsuInputManager inputManager)
{
@ -43,9 +43,7 @@ namespace osu.Game.Rulesets.Osu.UI
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
// The mouse button disable setting affects touch. It's a bit weird.
// This is mostly just doing the same as what is done in RulesetInputManager to match behaviour.
mouseDisabled = config.GetBindable<bool>(OsuSetting.MouseDisableButtons);
tapsDisabled = config.GetBindable<bool>(OsuSetting.TouchDisableGameplayTaps);
}
// Required to handle touches outside of the playfield when screen scaling is enabled.
@ -64,7 +62,7 @@ namespace osu.Game.Rulesets.Osu.UI
: OsuAction.LeftButton;
// Ignore any taps which trigger an action which is already handled. But track them for potential positional input in the future.
bool shouldResultInAction = osuInputManager.AllowGameplayInputs && !mouseDisabled.Value && trackedTouches.All(t => t.Action != action);
bool shouldResultInAction = osuInputManager.AllowGameplayInputs && !tapsDisabled.Value && trackedTouches.All(t => t.Action != action);
// If we can actually accept as an action, check whether this tap was on a circle's receptor.
// This case gets special handling to allow for empty-space stream tapping.

View File

@ -174,8 +174,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
private class TestLegacySkin : LegacySkin
{
public TestLegacySkin(IResourceStore<byte[]> storage, string fileName)
: base(new SkinInfo { Name = "Test Skin", Creator = "Craftplacer" }, null, storage, fileName)
public TestLegacySkin(IResourceStore<byte[]> fallbackStore, string fileName)
: base(new SkinInfo { Name = "Test Skin", Creator = "Craftplacer" }, null, fallbackStore, fileName)
{
}
}

View File

@ -147,11 +147,11 @@ namespace osu.Game.Tests.Mods
new Mod[] { new OsuModDeflate(), new OsuModApproachDifferent() },
new[] { typeof(OsuModDeflate), typeof(OsuModApproachDifferent) }
},
// system mod.
// system mod not applicable in lazer.
new object[]
{
new Mod[] { new OsuModHidden(), new OsuModTouchDevice() },
new[] { typeof(OsuModTouchDevice) }
new Mod[] { new OsuModHidden(), new ModScoreV2() },
new[] { typeof(ModScoreV2) }
},
// multi mod.
new object[]

View File

@ -11,9 +11,9 @@ namespace osu.Game.Tests.Rulesets.Scoring
[TestFixture]
public class HitResultTest
{
[TestCase(new[] { HitResult.Perfect, HitResult.Great, HitResult.Good, HitResult.Ok, HitResult.Meh }, new[] { HitResult.Miss })]
[TestCase(new[] { HitResult.LargeTickHit }, new[] { HitResult.LargeTickMiss })]
[TestCase(new[] { HitResult.SmallTickHit }, new[] { HitResult.SmallTickMiss })]
[TestCase(new[] { HitResult.Perfect, HitResult.Great, HitResult.Good, HitResult.Ok, HitResult.Meh }, new[] { HitResult.Miss, HitResult.IgnoreMiss })]
[TestCase(new[] { HitResult.LargeTickHit }, new[] { HitResult.LargeTickMiss, HitResult.IgnoreMiss })]
[TestCase(new[] { HitResult.SmallTickHit }, new[] { HitResult.SmallTickMiss, HitResult.IgnoreMiss })]
[TestCase(new[] { HitResult.LargeBonus, HitResult.SmallBonus }, new[] { HitResult.IgnoreMiss })]
[TestCase(new[] { HitResult.IgnoreHit }, new[] { HitResult.IgnoreMiss, HitResult.ComboBreak })]
public void TestValidResultPairs(HitResult[] maxResults, HitResult[] minResults)

View File

@ -13,6 +13,7 @@ using osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.IO;
using osu.Game.Skinning;
using osu.Game.Tests.Resources;
using SharpCompress.Archives.Zip;
namespace osu.Game.Tests.Skins.IO
@ -21,6 +22,25 @@ namespace osu.Game.Tests.Skins.IO
{
#region Testing filename metadata inclusion
[TestCase("Archives/modified-classic-20220723.osk")]
[TestCase("Archives/modified-default-20230117.osk")]
[TestCase("Archives/modified-argon-20231106.osk")]
public Task TestImportModifiedSkinHasResources(string archive) => runSkinTest(async osu =>
{
using (var stream = TestResources.OpenResource(archive))
{
var imported = await loadSkinIntoOsu(osu, new ImportTask(stream, "skin.osk"));
// When the import filename doesn't match, it should be appended (and update the skin.ini).
var skinManager = osu.Dependencies.Get<SkinManager>();
skinManager.CurrentSkinInfo.Value = imported;
Assert.That(skinManager.CurrentSkin.Value.LayoutInfos.Count, Is.EqualTo(2));
}
});
[Test]
public Task TestSingleImportDifferentFilename() => runSkinTest(async osu =>
{

View File

@ -15,6 +15,7 @@ using osu.Game.IO.Archives;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.HUD.HitErrorMeters;
using osu.Game.Skinning;
using osu.Game.Skinning.Components;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Skins
@ -57,6 +58,8 @@ namespace osu.Game.Tests.Skins
"Archives/modified-argon-pro-20231001.osk",
// Covers player name text component.
"Archives/modified-argon-20231106.osk",
// Covers "Argon" accuracy/score/combo counters, and wedges
"Archives/modified-argon-20231108.osk",
};
/// <summary>
@ -100,6 +103,20 @@ namespace osu.Game.Tests.Skins
}
}
[Test]
public void TestDeserialiseModifiedArgon()
{
using (var stream = TestResources.OpenResource("Archives/modified-argon-20231106.osk"))
using (var storage = new ZipArchiveReader(stream))
{
var skin = new TestSkin(new SkinInfo(), null, storage);
Assert.That(skin.LayoutInfos, Has.Count.EqualTo(2));
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(10));
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(PlayerName)));
}
}
[Test]
public void TestDeserialiseModifiedClassic()
{
@ -132,8 +149,8 @@ namespace osu.Game.Tests.Skins
private class TestSkin : Skin
{
public TestSkin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore<byte[]>? storage = null, string configurationFilename = "skin.ini")
: base(skin, resources, storage, configurationFilename)
public TestSkin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore<byte[]>? fallbackStore = null, string configurationFilename = "skin.ini")
: base(skin, resources, fallbackStore, configurationFilename)
{
}

View File

@ -95,8 +95,8 @@ namespace osu.Game.Tests.Skins
{
public const string SAMPLE_NAME = "test-sample";
public TestSkin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore<byte[]>? storage = null, string configurationFilename = "skin.ini")
: base(skin, resources, storage, configurationFilename)
public TestSkin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore<byte[]>? fallbackStore = null, string configurationFilename = "skin.ini")
: base(skin, resources, fallbackStore, configurationFilename)
{
}

View File

@ -181,6 +181,54 @@ namespace osu.Game.Tests.Visual.Background
AddStep("restore default beatmap", () => Beatmap.SetDefault());
}
[Test]
public void TestBeatmapBackgroundWithStoryboardUnloadedOnSuspension()
{
BackgroundScreenBeatmap nestedScreen = null;
setSupporter(true);
setSourceMode(BackgroundSource.BeatmapWithStoryboard);
AddStep("change beatmap", () => Beatmap.Value = createTestWorkingBeatmapWithStoryboard());
AddAssert("background changed", () => screen.CheckLastLoadChange() == true);
AddUntilStep("wait for beatmap background to be loaded", () => getCurrentBackground()?.GetType() == typeof(BeatmapBackgroundWithStoryboard));
AddUntilStep("storyboard present", () => screen.ChildrenOfType<DrawableStoryboard>().SingleOrDefault()?.IsLoaded == true);
AddStep("push new background to stack", () => stack.Push(nestedScreen = new BackgroundScreenBeatmap(Beatmap.Value)));
AddUntilStep("wait for screen to load", () => nestedScreen.IsLoaded && nestedScreen.IsCurrentScreen());
AddUntilStep("storyboard unloaded", () => !screen.ChildrenOfType<DrawableStoryboard>().Any());
AddStep("go back", () => screen.MakeCurrent());
AddUntilStep("storyboard reloaded", () => screen.ChildrenOfType<DrawableStoryboard>().SingleOrDefault()?.IsLoaded == true);
}
[Test]
public void TestBeatmapBackgroundWithStoryboardButBeatmapHasNone()
{
BackgroundScreenBeatmap nestedScreen = null;
setSupporter(true);
setSourceMode(BackgroundSource.BeatmapWithStoryboard);
AddStep("change beatmap", () => Beatmap.Value = createTestWorkingBeatmapWithUniqueBackground());
AddAssert("background changed", () => screen.CheckLastLoadChange() == true);
AddUntilStep("wait for beatmap background to be loaded", () => getCurrentBackground()?.GetType() == typeof(BeatmapBackgroundWithStoryboard));
AddUntilStep("no storyboard loaded", () => !screen.ChildrenOfType<DrawableStoryboard>().Any());
AddStep("push new background to stack", () => stack.Push(nestedScreen = new BackgroundScreenBeatmap(Beatmap.Value)));
AddUntilStep("wait for screen to load", () => nestedScreen.IsLoaded && nestedScreen.IsCurrentScreen());
AddUntilStep("still no storyboard", () => !screen.ChildrenOfType<DrawableStoryboard>().Any());
AddStep("go back", () => screen.MakeCurrent());
AddUntilStep("still no storyboard", () => !screen.ChildrenOfType<DrawableStoryboard>().Any());
}
[Test]
public void TestBackgroundTypeSwitch()
{

View File

@ -47,6 +47,7 @@ namespace osu.Game.Tests.Visual.Editing
CanScaleX = true,
CanScaleY = true,
CanScaleDiagonally = true,
CanFlipX = true,
CanFlipY = true,

View File

@ -47,17 +47,17 @@ namespace osu.Game.Tests.Visual.Gameplay
};
});
AddSliderStep("Width", 0, 1f, 1f, val =>
{
if (healthDisplay.IsNotNull())
healthDisplay.BarLength.Value = val;
});
AddSliderStep("Height", 0, 64, 0, val =>
{
if (healthDisplay.IsNotNull())
healthDisplay.BarHeight.Value = val;
});
AddSliderStep("Width", 0, 1f, 0.98f, val =>
{
if (healthDisplay.IsNotNull())
healthDisplay.Width = val;
});
}
[Test]

View File

@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
@ -96,7 +97,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("Begin drag top left", () =>
{
InputManager.MoveMouseTo(box1.ScreenSpaceDrawQuad.TopLeft - new Vector2(box1.ScreenSpaceDrawQuad.Width / 4));
InputManager.MoveMouseTo(box1.ScreenSpaceDrawQuad.TopLeft - new Vector2(box1.ScreenSpaceDrawQuad.Width / 4, box1.ScreenSpaceDrawQuad.Height / 8));
InputManager.PressButton(MouseButton.Left);
});
@ -146,8 +147,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
AddStep("Add big black box", () =>
{
InputManager.MoveMouseTo(skinEditor.ChildrenOfType<BigBlackBox>().First());
InputManager.Click(MouseButton.Left);
skinEditor.ChildrenOfType<SkinComponentToolbox.ToolboxComponentButton>().First(b => b.ChildrenOfType<BigBlackBox>().FirstOrDefault() != null).TriggerClick();
});
AddStep("store box", () =>
@ -243,7 +243,9 @@ namespace osu.Game.Tests.Visual.Gameplay
void revertAndCheckUnchanged()
{
AddStep("Revert changes", () => changeHandler.RestoreState(int.MinValue));
AddAssert("Current state is same as default", () => defaultState.SequenceEqual(changeHandler.GetCurrentState()));
AddAssert("Current state is same as default",
() => Encoding.UTF8.GetString(defaultState),
() => Is.EqualTo(Encoding.UTF8.GetString(changeHandler.GetCurrentState())));
}
}

View File

@ -17,6 +17,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Cached]
private ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset());
protected override Drawable CreateArgonImplementation() => new ArgonAccuracyCounter();
protected override Drawable CreateDefaultImplementation() => new DefaultAccuracyCounter();
protected override Drawable CreateLegacyImplementation() => new LegacyAccuracyCounter();

View File

@ -17,6 +17,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Cached]
private ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset());
protected override Drawable CreateArgonImplementation() => new ArgonComboCounter();
protected override Drawable CreateDefaultImplementation() => new DefaultComboCounter();
protected override Drawable CreateLegacyImplementation() => new LegacyComboCounter();

View File

@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Cached(typeof(HealthProcessor))]
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
protected override Drawable CreateArgonImplementation() => new ArgonHealthDisplay { Scale = new Vector2(0.6f) };
protected override Drawable CreateArgonImplementation() => new ArgonHealthDisplay { Scale = new Vector2(0.6f), Width = 1f };
protected override Drawable CreateDefaultImplementation() => new DefaultHealthDisplay { Scale = new Vector2(0.6f) };
protected override Drawable CreateLegacyImplementation() => new LegacyHealthDisplay { Scale = new Vector2(0.6f) };

View File

@ -17,6 +17,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Cached(typeof(ScoreProcessor))]
private ScoreProcessor scoreProcessor = TestGameplayState.Create(new OsuRuleset()).ScoreProcessor;
protected override Drawable CreateArgonImplementation() => new ArgonScoreCounter();
protected override Drawable CreateDefaultImplementation() => new DefaultScoreCounter();
protected override Drawable CreateLegacyImplementation() => new LegacyScoreCounter();

View File

@ -133,6 +133,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
private bool assertAllAvailableModsSelected()
{
var allAvailableMods = availableMods.Value
.Where(pair => pair.Key != ModType.System)
.SelectMany(pair => pair.Value)
.Where(mod => mod.UserPlayable && mod.HasImplementation)
.ToList();

View File

@ -6,11 +6,13 @@ using NUnit.Framework;
using osu.Framework.Extensions;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Overlays;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit;
@ -232,6 +234,35 @@ namespace osu.Game.Tests.Visual.Navigation
() => Is.EqualTo(beatmapSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0).ID));
}
[Test]
public void TestCreateNewDifficultyOnNonExistentBeatmap()
{
AddUntilStep("wait for dialog overlay", () => Game.ChildrenOfType<DialogOverlay>().SingleOrDefault() != null);
AddStep("open editor", () => Game.ChildrenOfType<ButtonSystem>().Single().OnEdit.Invoke());
AddUntilStep("wait for editor", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.IsLoaded);
AddStep("click on file", () =>
{
var item = getEditor().ChildrenOfType<Menu.DrawableMenuItem>().Single(i => i.Item.Text.Value.ToString() == "File");
item.TriggerClick();
});
AddStep("click on create new difficulty", () =>
{
var item = getEditor().ChildrenOfType<Menu.DrawableMenuItem>().Single(i => i.Item.Text.Value.ToString() == "Create new difficulty");
item.TriggerClick();
});
AddStep("click on catch", () =>
{
var item = getEditor().ChildrenOfType<Menu.DrawableMenuItem>().Single(i => i.Item.Text.Value.ToString() == "osu!catch");
item.TriggerClick();
});
AddAssert("save dialog displayed", () => Game.ChildrenOfType<DialogOverlay>().Single().CurrentDialog is SaveRequiredPopupDialog);
AddStep("press save", () => Game.ChildrenOfType<DialogOverlay>().Single().CurrentDialog!.PerformOkAction());
AddUntilStep("wait for editor", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.IsLoaded);
AddAssert("editor beatmap uses catch ruleset", () => getEditorBeatmap().BeatmapInfo.Ruleset.ShortName == "fruits");
}
private EditorBeatmap getEditorBeatmap() => getEditor().ChildrenOfType<EditorBeatmap>().Single();
private Editor getEditor() => (Editor)Game.ScreenStack.CurrentScreen;

View File

@ -12,6 +12,7 @@ using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
@ -835,6 +836,110 @@ namespace osu.Game.Tests.Visual.Navigation
AddAssert("exit dialog is shown", () => Game.Dependencies.Get<IDialogOverlay>().CurrentDialog is ConfirmExitDialog);
}
[Test]
public void TestTouchScreenDetectionAtSongSelect()
{
AddStep("touch logo", () =>
{
var button = Game.ChildrenOfType<OsuLogo>().Single();
var touch = new Touch(TouchSource.Touch1, button.ScreenSpaceDrawQuad.Centre);
InputManager.BeginTouch(touch);
InputManager.EndTouch(touch);
});
AddAssert("touch screen detected active", () => Game.Dependencies.Get<SessionStatics>().Get<bool>(Static.TouchInputActive), () => Is.True);
AddStep("click settings button", () =>
{
var button = Game.ChildrenOfType<MainMenuButton>().Last();
InputManager.MoveMouseTo(button);
InputManager.Click(MouseButton.Left);
});
AddAssert("touch screen detected inactive", () => Game.Dependencies.Get<SessionStatics>().Get<bool>(Static.TouchInputActive), () => Is.False);
AddStep("close settings sidebar", () => InputManager.Key(Key.Escape));
Screens.Select.SongSelect songSelect = null;
AddRepeatStep("go to solo", () => InputManager.Key(Key.P), 3);
AddUntilStep("wait for song select", () => (songSelect = Game.ScreenStack.CurrentScreen as Screens.Select.SongSelect) != null);
AddUntilStep("wait for beatmap sets loaded", () => songSelect.BeatmapSetsLoaded);
AddStep("switch to osu! ruleset", () =>
{
InputManager.PressKey(Key.LControl);
InputManager.Key(Key.Number1);
InputManager.ReleaseKey(Key.LControl);
});
AddStep("touch beatmap wedge", () =>
{
var wedge = Game.ChildrenOfType<BeatmapInfoWedge>().Single();
var touch = new Touch(TouchSource.Touch2, wedge.ScreenSpaceDrawQuad.Centre);
InputManager.BeginTouch(touch);
InputManager.EndTouch(touch);
});
AddUntilStep("touch device mod activated", () => Game.SelectedMods.Value, () => Has.One.InstanceOf<ModTouchDevice>());
AddStep("switch to mania ruleset", () =>
{
InputManager.PressKey(Key.LControl);
InputManager.Key(Key.Number4);
InputManager.ReleaseKey(Key.LControl);
});
AddUntilStep("touch device mod not activated", () => Game.SelectedMods.Value, () => Has.None.InstanceOf<ModTouchDevice>());
AddStep("touch beatmap wedge", () =>
{
var wedge = Game.ChildrenOfType<BeatmapInfoWedge>().Single();
var touch = new Touch(TouchSource.Touch2, wedge.ScreenSpaceDrawQuad.Centre);
InputManager.BeginTouch(touch);
InputManager.EndTouch(touch);
});
AddUntilStep("touch device mod not activated", () => Game.SelectedMods.Value, () => Has.None.InstanceOf<ModTouchDevice>());
AddStep("switch to osu! ruleset", () =>
{
InputManager.PressKey(Key.LControl);
InputManager.Key(Key.Number1);
InputManager.ReleaseKey(Key.LControl);
});
AddUntilStep("touch device mod activated", () => Game.SelectedMods.Value, () => Has.One.InstanceOf<ModTouchDevice>());
AddStep("click beatmap wedge", () =>
{
InputManager.MoveMouseTo(Game.ChildrenOfType<BeatmapInfoWedge>().Single());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("touch device mod not activated", () => Game.SelectedMods.Value, () => Has.None.InstanceOf<ModTouchDevice>());
}
[Test]
public void TestTouchScreenDetectionInGame()
{
PushAndConfirm(() => new TestPlaySongSelect());
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
AddStep("select", () => InputManager.Key(Key.Enter));
Player player = null;
AddUntilStep("wait for player", () =>
{
DismissAnyNotifications();
return (player = Game.ScreenStack.CurrentScreen as Player) != null;
});
AddUntilStep("wait for track playing", () => Game.Beatmap.Value.Track.IsRunning);
AddStep("touch", () =>
{
var touch = new Touch(TouchSource.Touch2, Game.ScreenSpaceDrawQuad.Centre);
InputManager.BeginTouch(touch);
InputManager.EndTouch(touch);
});
AddUntilStep("touch device mod added to score", () => player.Score.ScoreInfo.Mods, () => Has.One.InstanceOf<ModTouchDevice>());
AddStep("exit player", () => player.Exit());
AddUntilStep("touch device mod still active", () => Game.SelectedMods.Value, () => Has.One.InstanceOf<ModTouchDevice>());
}
private Func<Player> playToResults()
{
var player = playToCompletion();

View File

@ -64,7 +64,7 @@ namespace osu.Game.Tests.Visual.Online
new[] { "Plain", "This is plain comment" },
new[] { "Pinned", "This is pinned comment" },
new[] { "Link", "Please visit https://osu.ppy.sh" },
new[] { "Big Image", "![](Backgrounds/bg1)" },
new[] { "Big Image", "![](Backgrounds/bg1 \"Big Image\")" },
new[] { "Small Image", "![](Cursor/cursortrail)" },
new[]
{

View File

@ -17,18 +17,16 @@ namespace osu.Game.Tests.Visual.Online
{
BarGraph graph;
Children = new[]
Child = graph = new BarGraph
{
graph = new BarGraph
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(0.5f),
},
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(0.5f),
};
AddStep("values from 1-10", () => graph.Values = Enumerable.Range(1, 10).Select(i => (float)i));
AddStep("small values", () => graph.Values = Enumerable.Range(1, 10).Select(i => i * 0.01f).Concat(new[] { 100f }));
AddStep("values from 1-100", () => graph.Values = Enumerable.Range(1, 100).Select(i => (float)i));
AddStep("reversed values from 1-10", () => graph.Values = Enumerable.Range(1, 10).Reverse().Select(i => (float)i));
AddStep("empty values", () => graph.Values = Array.Empty<float>());
@ -36,6 +34,14 @@ namespace osu.Game.Tests.Visual.Online
AddStep("Top to bottom", () => graph.Direction = BarDirection.TopToBottom);
AddStep("Left to right", () => graph.Direction = BarDirection.LeftToRight);
AddStep("Right to left", () => graph.Direction = BarDirection.RightToLeft);
AddToggleStep("Toggle movement", enabled =>
{
if (enabled)
graph.MoveToY(-10, 1000).Then().MoveToY(10, 1000).Loop();
else
graph.ClearTransforms();
});
}
}
}

View File

@ -0,0 +1,86 @@
// 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.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Testing;
using osu.Game.Graphics.Cursor;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Users;
using osu.Game.Users.Drawables;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Tests.Visual.Online
{
public partial class TestSceneUserClickableAvatar : OsuManualInputManagerTestScene
{
[SetUp]
public void SetUp() => Schedule(() =>
{
Child = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Spacing = new Vector2(10f),
Children = new[]
{
generateUser(@"peppy", 2, CountryCode.AU, @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", false, "99EB47"),
generateUser(@"flyte", 3103765, CountryCode.JP, @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg", true),
generateUser(@"joshika39", 17032217, CountryCode.RS, @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", false),
new UpdateableAvatar(),
new UpdateableAvatar()
},
};
});
[Test]
public void TestClickableAvatarHover()
{
AddStep("hover avatar with user panel", () => InputManager.MoveMouseTo(this.ChildrenOfType<ClickableAvatar>().ElementAt(1)));
AddUntilStep("wait for tooltip to show", () => this.ChildrenOfType<ClickableAvatar.UserCardTooltip>().FirstOrDefault()?.State.Value == Visibility.Visible);
AddStep("hover out", () => InputManager.MoveMouseTo(new Vector2(0)));
AddUntilStep("wait for tooltip to hide", () => this.ChildrenOfType<ClickableAvatar.UserCardTooltip>().FirstOrDefault()?.State.Value == Visibility.Hidden);
AddStep("hover avatar without user panel", () => InputManager.MoveMouseTo(this.ChildrenOfType<ClickableAvatar>().ElementAt(0)));
AddUntilStep("wait for tooltip to show", () => this.ChildrenOfType<OsuTooltipContainer.OsuTooltip>().FirstOrDefault()?.State.Value == Visibility.Visible);
AddStep("hover out", () => InputManager.MoveMouseTo(new Vector2(0)));
AddUntilStep("wait for tooltip to hide", () => this.ChildrenOfType<OsuTooltipContainer.OsuTooltip>().FirstOrDefault()?.State.Value == Visibility.Hidden);
}
private Drawable generateUser(string username, int id, CountryCode countryCode, string cover, bool showPanel, string? color = null)
{
var user = new APIUser
{
Username = username,
Id = id,
CountryCode = countryCode,
CoverUrl = cover,
Colour = color ?? "000000",
Status =
{
Value = new UserStatusOnline()
},
};
return new ClickableAvatar(user, showPanel)
{
Width = 50,
Height = 50,
CornerRadius = 10,
Masking = true,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Radius = 1,
Colour = Color4.Black.Opacity(0.2f),
},
};
}
}
}

View File

@ -10,7 +10,6 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
@ -298,7 +297,7 @@ This is a line after the fenced code block!
{
public LinkInline Link;
public override MarkdownTextFlowContainer CreateTextFlow() => new TestMarkdownTextFlowContainer
public override OsuMarkdownTextFlowContainer CreateTextFlow() => new TestMarkdownTextFlowContainer
{
UrlAdded = link => Link = link,
};

View File

@ -203,6 +203,7 @@ namespace osu.Game.Tests.Visual.Ranking
public IBeatmap Beatmap { get; }
// ReSharper disable once NotNullOrRequiredMemberIsNotInitialized
public TestBeatmapConverter(IBeatmap beatmap)
{
Beatmap = beatmap;

View File

@ -167,7 +167,7 @@ namespace osu.Game.Tests.Visual.UserInterface
}
[Test]
public void TestAddingFlow()
public void TestAddingFlow([Values] bool withSystemModActive)
{
ModPresetColumn modPresetColumn = null!;
@ -181,7 +181,13 @@ namespace osu.Game.Tests.Visual.UserInterface
AddUntilStep("items loaded", () => modPresetColumn.IsLoaded && modPresetColumn.ItemsLoaded);
AddAssert("add preset button disabled", () => !this.ChildrenOfType<AddPresetButton>().Single().Enabled.Value);
AddStep("set mods", () => SelectedMods.Value = new Mod[] { new OsuModDaycore(), new OsuModClassic() });
AddStep("set mods", () =>
{
var newMods = new Mod[] { new OsuModDaycore(), new OsuModClassic() };
if (withSystemModActive)
newMods = newMods.Append(new OsuModTouchDevice()).ToArray();
SelectedMods.Value = newMods;
});
AddAssert("add preset button enabled", () => this.ChildrenOfType<AddPresetButton>().Single().Enabled.Value);
AddStep("click add preset button", () =>
@ -209,6 +215,9 @@ namespace osu.Game.Tests.Visual.UserInterface
});
AddUntilStep("popover closed", () => !this.ChildrenOfType<OsuPopover>().Any());
AddUntilStep("preset creation occurred", () => this.ChildrenOfType<ModPresetPanel>().Count() == 4);
AddAssert("preset has correct mods",
() => this.ChildrenOfType<ModPresetPanel>().Single(panel => panel.Preset.Value.Name == "new preset").Preset.Value.Mods,
() => Has.Count.EqualTo(2));
AddStep("click add preset button", () =>
{

View File

@ -86,6 +86,10 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("set mods to HD+HR+DT", () => SelectedMods.Value = new Mod[] { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime() });
AddAssert("panel is not active", () => !panel.AsNonNull().Active.Value);
// system mods are not included in presets.
AddStep("set mods to HR+DT+TD", () => SelectedMods.Value = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime(), new OsuModTouchDevice() });
AddAssert("panel is active", () => panel.AsNonNull().Active.Value);
}
[Test]
@ -113,6 +117,10 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("set customised mod", () => SelectedMods.Value = new[] { new OsuModDoubleTime { SpeedChange = { Value = 1.25 } } });
AddStep("activate panel", () => panel.AsNonNull().TriggerClick());
assertSelectedModsEquivalentTo(new Mod[] { new OsuModHardRock(), new OsuModDoubleTime { SpeedChange = { Value = 1.5 } } });
AddStep("set system mod", () => SelectedMods.Value = new[] { new OsuModTouchDevice() });
AddStep("activate panel", () => panel.AsNonNull().TriggerClick());
assertSelectedModsEquivalentTo(new Mod[] { new OsuModTouchDevice(), new OsuModHardRock(), new OsuModDoubleTime { SpeedChange = { Value = 1.5 } } });
}
private void assertSelectedModsEquivalentTo(IEnumerable<Mod> mods)

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;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
@ -17,6 +19,11 @@ namespace osu.Game.Tournament.Components
{
AutoSizeAxes = Axes.Both;
var players = team?.Players ?? new BindableList<TournamentUser>();
// split the players into two even columns, favouring the first column if odd.
int split = (int)Math.Ceiling(players.Count / 2f);
InternalChildren = new Drawable[]
{
new FillFlowContainer
@ -39,13 +46,13 @@ namespace osu.Game.Tournament.Components
{
Direction = FillDirection.Vertical,
AutoSizeAxes = Axes.Both,
ChildrenEnumerable = team?.Players.Select(createPlayerText).Take(5) ?? Enumerable.Empty<Drawable>()
ChildrenEnumerable = players.Take(split).Select(createPlayerText),
},
new FillFlowContainer
{
Direction = FillDirection.Vertical,
AutoSizeAxes = Axes.Both,
ChildrenEnumerable = team?.Players.Select(createPlayerText).Skip(5) ?? Enumerable.Empty<Drawable>()
ChildrenEnumerable = players.Skip(split).Select(createPlayerText),
},
}
},

View File

@ -74,7 +74,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Statistics
#region Tooltip implementation
public virtual ITooltip GetCustomTooltip() => null;
public virtual ITooltip GetCustomTooltip() => null!;
public virtual object TooltipContent => null;
#endregion

View File

@ -108,6 +108,8 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.MouseDisableWheel, false);
SetDefault(OsuSetting.ConfineMouseMode, OsuConfineMouseMode.DuringGameplay);
SetDefault(OsuSetting.TouchDisableGameplayTaps, false);
// Graphics
SetDefault(OsuSetting.ShowFpsDisplay, false);
@ -330,6 +332,10 @@ namespace osu.Game.Configuration
ShowHealthDisplayWhenCantFail,
FadePlayfieldWhenHealthLow,
/// <summary>
/// Disables mouse buttons clicks during gameplay.
/// </summary>
MouseDisableButtons,
MouseDisableWheel,
ConfineMouseMode,
@ -408,6 +414,7 @@ namespace osu.Game.Configuration
EditorLimitedDistanceSnap,
ReplaySettingsOverlay,
AutomaticallyDownloadMissingBeatmaps,
EditorShowSpeedChanges
EditorShowSpeedChanges,
TouchDisableGameplayTaps,
}
}

View File

@ -3,7 +3,9 @@
#nullable disable
using osu.Framework;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
@ -24,6 +26,7 @@ namespace osu.Game.Configuration
SetDefault(Static.LastHoverSoundPlaybackTime, (double?)null);
SetDefault(Static.LastModSelectPanelSamplePlaybackTime, (double?)null);
SetDefault<APISeasonalBackgrounds>(Static.SeasonalBackgrounds, null);
SetDefault(Static.TouchInputActive, RuntimeInfo.IsMobile);
}
/// <summary>
@ -63,6 +66,12 @@ namespace osu.Game.Configuration
/// The last playback time in milliseconds of an on/off sample (from <see cref="ModSelectPanel"/>).
/// Used to debounce <see cref="ModSelectPanel"/> on/off sounds game-wide to avoid volume saturation, especially in activating mod presets with many mods.
/// </summary>
LastModSelectPanelSamplePlaybackTime
LastModSelectPanelSamplePlaybackTime,
/// <summary>
/// Whether the last positional input received was a touch input.
/// Used in touchscreen detection scenarios (<see cref="TouchInputInterceptor"/>).
/// </summary>
TouchInputActive,
}
}

View File

@ -2,6 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@ -10,6 +12,7 @@ using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Overlays;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens;
using osu.Game.Storyboards.Drawables;
namespace osu.Game.Graphics.Backgrounds
@ -18,6 +21,10 @@ namespace osu.Game.Graphics.Backgrounds
{
private readonly InterpolatingFramedClock storyboardClock;
private AudioContainer storyboardContainer = null!;
private DrawableStoryboard? drawableStoryboard;
private CancellationTokenSource? loadCancellationSource = new CancellationTokenSource();
[Resolved(CanBeNull = true)]
private MusicController? musicController { get; set; }
@ -33,18 +40,59 @@ namespace osu.Game.Graphics.Backgrounds
[BackgroundDependencyLoader]
private void load()
{
if (!Beatmap.Storyboard.HasDrawable)
return;
if (Beatmap.Storyboard.ReplacesBackground)
Sprite.Alpha = 0;
LoadComponentAsync(new AudioContainer
AddInternal(storyboardContainer = new AudioContainer
{
RelativeSizeAxes = Axes.Both,
Volume = { Value = 0 },
Child = new DrawableStoryboard(Beatmap.Storyboard, mods.Value) { Clock = storyboardClock }
}, AddInternal);
});
LoadStoryboard(false);
}
public void LoadStoryboard(bool async = true)
{
Debug.Assert(drawableStoryboard == null);
if (!Beatmap.Storyboard.HasDrawable)
return;
drawableStoryboard = new DrawableStoryboard(Beatmap.Storyboard, mods.Value)
{
Clock = storyboardClock
};
if (async)
LoadComponentAsync(drawableStoryboard, finishLoad, (loadCancellationSource = new CancellationTokenSource()).Token);
else
{
LoadComponent(drawableStoryboard);
finishLoad(drawableStoryboard);
}
void finishLoad(DrawableStoryboard s)
{
if (Beatmap.Storyboard.ReplacesBackground)
Sprite.FadeOut(BackgroundScreen.TRANSITION_LENGTH, Easing.InQuint);
storyboardContainer.FadeInFromZero(BackgroundScreen.TRANSITION_LENGTH, Easing.OutQuint);
storyboardContainer.Add(s);
}
}
public void UnloadStoryboard()
{
if (drawableStoryboard == null)
return;
loadCancellationSource?.Cancel();
loadCancellationSource = null;
// clear is intentionally used here for the storyboard to be disposed asynchronously.
storyboardContainer.Clear();
drawableStoryboard = null;
Sprite.Alpha = 1f;
}
protected override void LoadComplete()

View File

@ -5,7 +5,6 @@ using Markdig.Extensions.Footnotes;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Game.Overlays;
@ -62,7 +61,7 @@ namespace osu.Game.Graphics.Containers.Markdown.Footnotes
lastFootnote = Text = footnote;
}
public override MarkdownTextFlowContainer CreateTextFlow() => new FootnoteMarkdownTextFlowContainer();
public override OsuMarkdownTextFlowContainer CreateTextFlow() => new FootnoteMarkdownTextFlowContainer();
}
private partial class FootnoteMarkdownTextFlowContainer : OsuMarkdownTextFlowContainer

View File

@ -63,7 +63,7 @@ namespace osu.Game.Graphics.Containers.Markdown
Font = OsuFont.GetFont(Typeface.Inter, size: 14, weight: FontWeight.Regular),
};
public override MarkdownTextFlowContainer CreateTextFlow() => new OsuMarkdownTextFlowContainer();
public override OsuMarkdownTextFlowContainer CreateTextFlow() => new OsuMarkdownTextFlowContainer();
protected override MarkdownHeading CreateHeading(HeadingBlock headingBlock) => new OsuMarkdownHeading(headingBlock);

View File

@ -145,6 +145,13 @@ namespace osu.Game.Graphics.UserInterface
float barHeight = drawSize.Y * ((direction == BarDirection.TopToBottom || direction == BarDirection.BottomToTop) ? lengths[i] : barBreadth);
float barWidth = drawSize.X * ((direction == BarDirection.LeftToRight || direction == BarDirection.RightToLeft) ? lengths[i] : barBreadth);
if (barHeight == 0 || barWidth == 0)
continue;
// Apply minimum sizing to hide the fact that we don't have fractional anti-aliasing.
barHeight = Math.Max(barHeight, 1.5f);
barWidth = Math.Max(barWidth, 1.5f);
Vector2 topLeft;
switch (direction)

View File

@ -0,0 +1,72 @@
// 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.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics;
using osu.Framework.Input.Events;
using osu.Framework.Input.StateChanges;
using osu.Framework.Logging;
using osu.Game.Configuration;
using osuTK;
using osuTK.Input;
namespace osu.Game.Input
{
/// <summary>
/// Intercepts all positional input events and sets the appropriate <see cref="Static.TouchInputActive"/> value
/// for consumption by particular game screens.
/// </summary>
public partial class TouchInputInterceptor : Component
{
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
private readonly BindableBool touchInputActive = new BindableBool();
[BackgroundDependencyLoader]
private void load(SessionStatics statics)
{
statics.BindWith(Static.TouchInputActive, touchInputActive);
}
protected override bool Handle(UIEvent e)
{
bool touchInputWasActive = touchInputActive.Value;
switch (e)
{
case MouseEvent:
if (e.CurrentState.Mouse.LastSource is not ISourcedFromTouch)
{
if (touchInputWasActive)
Logger.Log($@"Touch input deactivated due to received {e.GetType().ReadableName()}", LoggingTarget.Input);
touchInputActive.Value = false;
}
break;
case TouchEvent:
if (!touchInputWasActive)
Logger.Log($@"Touch input activated due to received {e.GetType().ReadableName()}", LoggingTarget.Input);
touchInputActive.Value = true;
break;
case KeyDownEvent keyDown:
if (keyDown.Key == Key.T && keyDown.ControlPressed && keyDown.ShiftPressed)
debugToggleTouchInputActive();
break;
}
return false;
}
[Conditional("TOUCH_INPUT_DEBUG")]
private void debugToggleTouchInputActive()
{
Logger.Log($@"Debug-toggling touch input to {(touchInputActive.Value ? @"inactive" : @"active")}", LoggingTarget.Information, LogLevel.Important);
touchInputActive.Toggle();
}
}
}

View File

@ -40,14 +40,14 @@ namespace osu.Game.Localisation
public static LocalisableString DisableMouseWheelVolumeAdjust => new TranslatableString(getKey(@"disable_mouse_wheel_volume_adjust"), @"Disable mouse wheel adjusting volume during gameplay");
/// <summary>
/// "Volume can still be adjusted using the mouse wheel by holding "Alt""
/// "Volume can still be adjusted using the mouse wheel by holding &quot;Alt&quot;"
/// </summary>
public static LocalisableString DisableMouseWheelVolumeAdjustTooltip => new TranslatableString(getKey(@"disable_mouse_wheel_volume_adjust_tooltip"), @"Volume can still be adjusted using the mouse wheel by holding ""Alt""");
/// <summary>
/// "Disable mouse buttons during gameplay"
/// "Disable clicks during gameplay"
/// </summary>
public static LocalisableString DisableMouseButtons => new TranslatableString(getKey(@"disable_mouse_buttons"), @"Disable mouse buttons during gameplay");
public static LocalisableString DisableClicksDuringGameplay => new TranslatableString(getKey(@"disable_clicks"), @"Disable clicks during gameplay");
/// <summary>
/// "Enable high precision mouse to adjust sensitivity"

View File

@ -12,43 +12,53 @@ namespace osu.Game.Localisation.SkinComponents
/// <summary>
/// "Sprite name"
/// </summary>
public static LocalisableString SpriteName => new TranslatableString(getKey(@"sprite_name"), "Sprite name");
public static LocalisableString SpriteName => new TranslatableString(getKey(@"sprite_name"), @"Sprite name");
/// <summary>
/// "The filename of the sprite"
/// </summary>
public static LocalisableString SpriteNameDescription => new TranslatableString(getKey(@"sprite_name_description"), "The filename of the sprite");
public static LocalisableString SpriteNameDescription => new TranslatableString(getKey(@"sprite_name_description"), @"The filename of the sprite");
/// <summary>
/// "Font"
/// </summary>
public static LocalisableString Font => new TranslatableString(getKey(@"font"), "Font");
public static LocalisableString Font => new TranslatableString(getKey(@"font"), @"Font");
/// <summary>
/// "The font to use."
/// </summary>
public static LocalisableString FontDescription => new TranslatableString(getKey(@"font_description"), "The font to use.");
public static LocalisableString FontDescription => new TranslatableString(getKey(@"font_description"), @"The font to use.");
/// <summary>
/// "Text"
/// </summary>
public static LocalisableString TextElementText => new TranslatableString(getKey(@"text_element_text"), "Text");
public static LocalisableString TextElementText => new TranslatableString(getKey(@"text_element_text"), @"Text");
/// <summary>
/// "The text to be displayed."
/// </summary>
public static LocalisableString TextElementTextDescription => new TranslatableString(getKey(@"text_element_text_description"), "The text to be displayed.");
public static LocalisableString TextElementTextDescription => new TranslatableString(getKey(@"text_element_text_description"), @"The text to be displayed.");
/// <summary>
/// "Corner radius"
/// </summary>
public static LocalisableString CornerRadius => new TranslatableString(getKey(@"corner_radius"), "Corner radius");
public static LocalisableString CornerRadius => new TranslatableString(getKey(@"corner_radius"), @"Corner radius");
/// <summary>
/// "How rounded the corners should be."
/// </summary>
public static LocalisableString CornerRadiusDescription => new TranslatableString(getKey(@"corner_radius_description"), "How rounded the corners should be.");
public static LocalisableString CornerRadiusDescription => new TranslatableString(getKey(@"corner_radius_description"), @"How rounded the corners should be.");
private static string getKey(string key) => $"{prefix}:{key}";
/// <summary>
/// "Show label"
/// </summary>
public static LocalisableString ShowLabel => new TranslatableString(getKey(@"show_label"), @"Show label");
/// <summary>
/// "Whether the component&#39;s label should be shown."
/// </summary>
public static LocalisableString ShowLabelDescription => new TranslatableString(getKey(@"show_label_description"), @"Whether the component's label should be shown.");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -0,0 +1,24 @@
// 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.Localisation;
namespace osu.Game.Localisation
{
public static class TouchSettingsStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.TouchSettings";
/// <summary>
/// "Touch"
/// </summary>
public static LocalisableString Touch => new TranslatableString(getKey(@"touch"), @"Touch");
/// <summary>
/// "Disable taps during gameplay"
/// </summary>
public static LocalisableString DisableTapsDuringGameplay => new TranslatableString(getKey(@"disable_taps_during_gameplay"), @"Disable taps during gameplay");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -407,6 +407,8 @@ namespace osu.Game
})
});
base.Content.Add(new TouchInputInterceptor());
KeyBindingStore = new RealmKeyBindingStore(realm, keyCombinationProvider);
KeyBindingStore.Register(globalBindings, RulesetStore.AvailableRulesets);
@ -575,14 +577,14 @@ namespace osu.Game
case JoystickHandler jh:
return new JoystickSettings(jh);
case TouchHandler th:
return new TouchSettings(th);
}
}
switch (handler)
{
case TouchHandler th:
return new TouchSettings(th);
case MidiHandler:
return new InputSection.HandlerSection(handler);

View File

@ -55,7 +55,7 @@ namespace osu.Game.Overlays.BeatmapSet
AutoSizeAxes = Axes.Both,
CornerRadius = 4,
Masking = true,
Child = avatar = new UpdateableAvatar(showGuestOnNull: false)
Child = avatar = new UpdateableAvatar(showUserPanelOnHover: true, showGuestOnNull: false)
{
Size = new Vector2(height),
},

View File

@ -22,7 +22,7 @@ namespace osu.Game.Overlays.Comments
protected override MarkdownHeading CreateHeading(HeadingBlock headingBlock) => new CommentMarkdownHeading(headingBlock);
public override MarkdownTextFlowContainer CreateTextFlow() => new CommentMarkdownTextFlowContainer();
public override OsuMarkdownTextFlowContainer CreateTextFlow() => new CommentMarkdownTextFlowContainer();
private partial class CommentMarkdownHeading : OsuMarkdownHeading
{
@ -49,14 +49,14 @@ namespace osu.Game.Overlays.Comments
}
}
private partial class CommentMarkdownTextFlowContainer : MarkdownTextFlowContainer
private partial class CommentMarkdownTextFlowContainer : OsuMarkdownTextFlowContainer
{
protected override void AddImage(LinkInline linkInline) => AddDrawable(new CommentMarkdownImage(linkInline.Url));
protected override void AddImage(LinkInline linkInline) => AddDrawable(new CommentMarkdownImage(linkInline));
private partial class CommentMarkdownImage : MarkdownImage
private partial class CommentMarkdownImage : OsuMarkdownImage
{
public CommentMarkdownImage(string url)
: base(url)
public CommentMarkdownImage(LinkInline linkInline)
: base(linkInline)
{
}

View File

@ -102,7 +102,7 @@ namespace osu.Game.Overlays.Comments
Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING, Vertical = 20 },
Children = new Drawable[]
{
avatar = new UpdateableAvatar(api.LocalUser.Value)
avatar = new UpdateableAvatar(api.LocalUser.Value, isInteractive: false)
{
Size = new Vector2(50),
CornerExponent = 2,

View File

@ -144,7 +144,7 @@ namespace osu.Game.Overlays.Comments
Size = new Vector2(avatar_size),
Children = new Drawable[]
{
new UpdateableAvatar(Comment.User)
new UpdateableAvatar(Comment.User, showUserPanelOnHover: true)
{
Size = new Vector2(avatar_size),
Masking = true,

View File

@ -115,7 +115,7 @@ namespace osu.Game.Overlays.Mods
{
Name = nameTextBox.Current.Value,
Description = descriptionTextBox.Current.Value,
Mods = selectedMods.Value.ToArray(),
Mods = selectedMods.Value.Where(mod => mod.Type != ModType.System).ToArray(),
Ruleset = r.Find<RulesetInfo>(ruleset.Value.ShortName)!
}));

View File

@ -153,7 +153,7 @@ namespace osu.Game.Overlays.Mods
private void useCurrentMods()
{
saveableMods = selectedMods.Value.ToHashSet();
saveableMods = selectedMods.Value.Where(mod => mod.Type != ModType.System).ToHashSet();
updateState();
}
@ -168,7 +168,7 @@ namespace osu.Game.Overlays.Mods
if (!selectedMods.Value.Any())
return false;
return !saveableMods.SetEquals(selectedMods.Value);
return !saveableMods.SetEquals(selectedMods.Value.Where(mod => mod.Type != ModType.System));
}
private void save()

View File

@ -1,7 +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.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
@ -56,17 +55,14 @@ namespace osu.Game.Overlays.Mods
protected override void Select()
{
// if the preset is not active at the point of the user click, then set the mods using the preset directly, discarding any previous selections,
// which will also have the side effect of activating the preset (see `updateActiveState()`).
selectedMods.Value = Preset.Value.Mods.ToArray();
var selectedSystemMods = selectedMods.Value.Where(mod => mod.Type == ModType.System);
// will also have the side effect of activating the preset (see `updateActiveState()`).
selectedMods.Value = Preset.Value.Mods.Concat(selectedSystemMods).ToArray();
}
protected override void Deselect()
{
// if the preset is active when the user has clicked it, then it means that the set of active mods is exactly equal to the set of mods in the preset
// (there are no other active mods than what the preset specifies, and the mod settings match exactly).
// therefore it's safe to just clear selected mods, since it will have the effect of toggling the preset off.
selectedMods.Value = Array.Empty<Mod>();
selectedMods.Value = selectedMods.Value.Except(Preset.Value.Mods).ToArray();
}
private void selectedModsChanged()
@ -79,7 +75,7 @@ namespace osu.Game.Overlays.Mods
private void updateActiveState()
{
Active.Value = new HashSet<Mod>(Preset.Value.Mods).SetEquals(selectedMods.Value);
Active.Value = new HashSet<Mod>(Preset.Value.Mods).SetEquals(selectedMods.Value.Where(mod => mod.Type != ModType.System));
}
#region Filtering support

View File

@ -41,6 +41,7 @@ namespace osu.Game.Overlays.Mods
private void updateEnabledState()
{
Enabled.Value = availableMods.Value
.Where(pair => pair.Key != ModType.System)
.SelectMany(pair => pair.Value)
.Any(modState => !modState.Active.Value && modState.Visible);
}

View File

@ -75,7 +75,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
},
new SettingsCheckbox
{
LabelText = MouseSettingsStrings.DisableMouseButtons,
LabelText = MouseSettingsStrings.DisableClicksDuringGameplay,
Current = osuConfig.GetBindable<bool>(OsuSetting.MouseDisableButtons)
},
};

View File

@ -3,38 +3,48 @@
using System.Collections.Generic;
using System.Linq;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Input.Handlers.Touch;
using osu.Framework.Input.Handlers;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Localisation;
namespace osu.Game.Overlays.Settings.Sections.Input
{
/// <summary>
/// Touch input settings subsection common to all touch handlers (even on different platforms).
/// </summary>
public partial class TouchSettings : SettingsSubsection
{
private readonly TouchHandler handler;
private readonly InputHandler handler;
public TouchSettings(TouchHandler handler)
protected override LocalisableString Header => TouchSettingsStrings.Touch;
public TouchSettings(InputHandler handler)
{
this.handler = handler;
}
[BackgroundDependencyLoader]
private void load()
private void load(OsuConfigManager osuConfig)
{
Children = new Drawable[]
if (!RuntimeInfo.IsMobile) // don't allow disabling the only input method (touch) on mobile.
{
new SettingsCheckbox
Add(new SettingsCheckbox
{
LabelText = CommonStrings.Enabled,
Current = handler.Enabled
},
};
});
}
Add(new SettingsCheckbox
{
LabelText = TouchSettingsStrings.DisableTapsDuringGameplay,
Current = osuConfig.GetBindable<bool>(OsuSetting.TouchDisableGameplayTaps)
});
}
public override IEnumerable<LocalisableString> FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { @"touchscreen" });
protected override LocalisableString Header => handler.Description;
}
}

View File

@ -8,6 +8,7 @@ using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@ -153,6 +154,8 @@ namespace osu.Game.Overlays.SkinEditor
Items = new[]
{
new EditorMenuItem(Web.CommonStrings.ButtonsSave, MenuItemType.Standard, () => Save()),
new EditorMenuItem(CommonStrings.Export, MenuItemType.Standard, () => skins.ExportCurrentSkin()) { Action = { Disabled = !RuntimeInfo.IsDesktop } },
new EditorMenuItemSpacer(),
new EditorMenuItem(CommonStrings.RevertToDefault, MenuItemType.Destructive, () => dialogOverlay?.Push(new RevertConfirmDialog(revert))),
new EditorMenuItemSpacer(),
new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, () => skinEditorOverlay?.Hide()),
@ -406,7 +409,14 @@ namespace osu.Game.Overlays.SkinEditor
cp.Colour = colours.Yellow;
});
changeHandler?.Dispose();
skins.EnsureMutableSkin();
var targetContainer = getTarget(selectedTarget.Value);
if (targetContainer != null)
changeHandler = new SkinEditorChangeHandler(targetContainer);
hasBegunMutating = true;
}

View File

@ -7,6 +7,7 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Utils;
@ -31,8 +32,44 @@ namespace osu.Game.Overlays.SkinEditor
UpdatePosition = updateDrawablePosition
};
private bool allSelectedSupportManualSizing(Axes axis) => SelectedItems.All(b => (b as CompositeDrawable)?.AutoSizeAxes.HasFlagFast(axis) == false);
public override bool HandleScale(Vector2 scale, Anchor anchor)
{
Axes adjustAxis;
switch (anchor)
{
// for corners, adjust scale.
case Anchor.TopLeft:
case Anchor.TopRight:
case Anchor.BottomLeft:
case Anchor.BottomRight:
adjustAxis = Axes.Both;
break;
// for edges, adjust size.
// autosize elements can't be easily handled so just disable sizing for now.
case Anchor.TopCentre:
case Anchor.BottomCentre:
if (!allSelectedSupportManualSizing(Axes.Y))
return false;
adjustAxis = Axes.Y;
break;
case Anchor.CentreLeft:
case Anchor.CentreRight:
if (!allSelectedSupportManualSizing(Axes.X))
return false;
adjustAxis = Axes.X;
break;
default:
throw new ArgumentOutOfRangeException(nameof(anchor), anchor, null);
}
// convert scale to screen space
scale = ToScreenSpace(scale) - ToScreenSpace(Vector2.Zero);
@ -120,7 +157,20 @@ namespace osu.Game.Overlays.SkinEditor
if (Precision.AlmostEquals(MathF.Abs(drawableItem.Rotation) % 180, 90))
currentScaledDelta = new Vector2(scaledDelta.Y, scaledDelta.X);
drawableItem.Scale *= currentScaledDelta;
switch (adjustAxis)
{
case Axes.X:
drawableItem.Width *= currentScaledDelta.X;
break;
case Axes.Y:
drawableItem.Height *= currentScaledDelta.Y;
break;
case Axes.Both:
drawableItem.Scale *= currentScaledDelta;
break;
}
}
return true;
@ -169,8 +219,9 @@ namespace osu.Game.Overlays.SkinEditor
{
base.OnSelectionChanged();
SelectionBox.CanScaleX = true;
SelectionBox.CanScaleY = true;
SelectionBox.CanScaleX = allSelectedSupportManualSizing(Axes.X);
SelectionBox.CanScaleY = allSelectedSupportManualSizing(Axes.Y);
SelectionBox.CanScaleDiagonally = true;
SelectionBox.CanFlipX = true;
SelectionBox.CanFlipY = true;
SelectionBox.CanReverse = false;
@ -215,7 +266,15 @@ namespace osu.Game.Overlays.SkinEditor
yield return new OsuMenuItem("Reset scale", MenuItemType.Standard, () =>
{
foreach (var blueprint in SelectedBlueprints)
((Drawable)blueprint.Item).Scale = Vector2.One;
{
var blueprintItem = ((Drawable)blueprint.Item);
blueprintItem.Scale = Vector2.One;
if (blueprintItem.RelativeSizeAxes.HasFlagFast(Axes.X))
blueprintItem.Width = 1;
if (blueprintItem.RelativeSizeAxes.HasFlagFast(Axes.Y))
blueprintItem.Height = 1;
}
});
yield return new EditorMenuItemSpacer();

View File

@ -7,7 +7,6 @@ using Markdig.Extensions.Yaml;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Containers.Markdown;
using osu.Game.Graphics.Containers.Markdown;
namespace osu.Game.Overlays.Wiki.Markdown
@ -53,7 +52,7 @@ namespace osu.Game.Overlays.Wiki.Markdown
base.AddMarkdownComponent(markdownObject, container, level);
}
public override MarkdownTextFlowContainer CreateTextFlow() => new WikiMarkdownTextFlowContainer();
public override OsuMarkdownTextFlowContainer CreateTextFlow() => new WikiMarkdownTextFlowContainer();
private partial class WikiMarkdownTextFlowContainer : OsuMarkdownTextFlowContainer
{

View File

@ -93,7 +93,7 @@ namespace osu.Game.Overlays.Wiki
public override SpriteText CreateSpriteText() => base.CreateSpriteText().With(t => t.Font = t.Font.With(Typeface.Torus, weight: FontWeight.Bold));
public override MarkdownTextFlowContainer CreateTextFlow() => base.CreateTextFlow().With(f => f.TextAnchor = Anchor.TopCentre);
public override OsuMarkdownTextFlowContainer CreateTextFlow() => base.CreateTextFlow().With(f => f.TextAnchor = Anchor.TopCentre);
protected override MarkdownParagraph CreateParagraph(ParagraphBlock paragraphBlock, int level)
=> base.CreateParagraph(paragraphBlock, level).With(p => p.Margin = new MarginPadding { Bottom = 10 });

View File

@ -59,6 +59,13 @@ namespace osu.Game.Rulesets.Mods
/// </summary>
bool ValidForMultiplayerAsFreeMod { get; }
/// <summary>
/// Indicates that this mod is always permitted in scenarios wherein a user is submitting a score regardless of other circumstances.
/// Intended for mods that are informational in nature and do not really affect gameplay by themselves,
/// but are more of a gauge of increased/decreased difficulty due to the user's configuration (e.g. <see cref="ModTouchDevice"/>).
/// </summary>
bool AlwaysValidForSubmission { get; }
/// <summary>
/// Create a fresh <see cref="Mod"/> instance based on this mod.
/// </summary>

View File

@ -156,6 +156,10 @@ namespace osu.Game.Rulesets.Mods
[JsonIgnore]
public virtual bool ValidForMultiplayerAsFreeMod => true;
/// <inheritdoc/>
[JsonIgnore]
public virtual bool AlwaysValidForSubmission => false;
/// <summary>
/// Whether this mod requires configuration to apply changes to the game.
/// </summary>

View File

@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Mods
public sealed override bool ValidForMultiplayer => false;
public sealed override bool ValidForMultiplayerAsFreeMod => false;
public override Type[] IncompatibleMods => new[] { typeof(ModCinema), typeof(ModRelax), typeof(ModAdaptiveSpeed) };
public override Type[] IncompatibleMods => new[] { typeof(ModCinema), typeof(ModRelax), typeof(ModAdaptiveSpeed), typeof(ModTouchDevice) };
public override bool HasImplementation => GetType().GenericTypeArguments.Length == 0;

View File

@ -0,0 +1,24 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
namespace osu.Game.Rulesets.Mods
{
public class ModTouchDevice : Mod, IApplicableMod
{
public sealed override string Name => "Touch Device";
public sealed override string Acronym => "TD";
public sealed override IconUsage? Icon => OsuIcon.PlayStyleTouch;
public sealed override LocalisableString Description => "Automatically applied to plays on devices with a touchscreen.";
public sealed override double ScoreMultiplier => 1;
public sealed override ModType Type => ModType.System;
public sealed override bool ValidForMultiplayer => false;
public sealed override bool ValidForMultiplayerAsFreeMod => false;
public sealed override bool AlwaysValidForSubmission => true;
public override Type[] IncompatibleMods => new[] { typeof(ICreateReplayData) };
}
}

View File

@ -204,6 +204,8 @@ namespace osu.Game.Rulesets
public ModAutoplay? GetAutoplayMod() => CreateMod<ModAutoplay>();
public ModTouchDevice? GetTouchDeviceMod() => CreateMod<ModTouchDevice>();
/// <summary>
/// Create a transformer which adds lookups specific to a ruleset to skin sources.
/// </summary>

View File

@ -350,6 +350,9 @@ namespace osu.Game.Rulesets.Scoring
if (maxResult.IsBonus() && minResult != HitResult.IgnoreMiss)
throw new ArgumentOutOfRangeException(nameof(minResult), $"{HitResult.IgnoreMiss} is the only valid minimum result for a {maxResult} judgement.");
if (minResult == HitResult.IgnoreMiss)
return;
if (maxResult == HitResult.LargeTickHit && minResult != HitResult.LargeTickMiss)
throw new ArgumentOutOfRangeException(nameof(minResult), $"{HitResult.LargeTickMiss} is the only valid minimum result for a {maxResult} judgement.");

View File

@ -72,6 +72,7 @@ namespace osu.Game.Rulesets.UI
private void load(OsuConfigManager config)
{
mouseDisabled = config.GetBindable<bool>(OsuSetting.MouseDisableButtons);
tapsDisabled = config.GetBindable<bool>(OsuSetting.TouchDisableGameplayTaps);
}
#region Action mapping (for replays)
@ -124,6 +125,7 @@ namespace osu.Game.Rulesets.UI
#region Setting application (disables etc.)
private Bindable<bool> mouseDisabled;
private Bindable<bool> tapsDisabled;
protected override bool Handle(UIEvent e)
{
@ -147,9 +149,9 @@ namespace osu.Game.Rulesets.UI
protected override bool HandleMouseTouchStateChange(TouchStateChangeEvent e)
{
if (mouseDisabled.Value)
if (tapsDisabled.Value)
{
// Only propagate positional data when mouse buttons are disabled.
// Only propagate positional data when taps are disabled.
e = new TouchStateChangeEvent(e.State, e.Input, e.Touch, false, e.LastPosition);
}

View File

@ -13,7 +13,8 @@ namespace osu.Game.Screens
{
public abstract partial class BackgroundScreen : Screen, IEquatable<BackgroundScreen>
{
protected const float TRANSITION_LENGTH = 500;
public const float TRANSITION_LENGTH = 500;
private const float x_movement_amount = 50;
private readonly bool animateOnEnter;

View File

@ -3,11 +3,14 @@
#nullable disable
using System.Diagnostics;
using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
@ -34,6 +37,9 @@ namespace osu.Game.Screens.Backgrounds
[Resolved]
private IBindable<WorkingBeatmap> beatmap { get; set; }
[Resolved]
private GameHost gameHost { get; set; }
protected virtual bool AllowStoryboardBackground => true;
public BackgroundScreenDefault(bool animateOnEnter = true)
@ -71,6 +77,34 @@ namespace osu.Game.Screens.Backgrounds
void next() => Next();
}
private ScheduledDelegate storyboardUnloadDelegate;
public override void OnSuspending(ScreenTransitionEvent e)
{
var backgroundScreenStack = Parent as BackgroundScreenStack;
Debug.Assert(backgroundScreenStack != null);
if (background is BeatmapBackgroundWithStoryboard storyboardBackground)
storyboardUnloadDelegate = gameHost.UpdateThread.Scheduler.AddDelayed(storyboardBackground.UnloadStoryboard, TRANSITION_LENGTH);
base.OnSuspending(e);
}
public override void OnResuming(ScreenTransitionEvent e)
{
if (background is BeatmapBackgroundWithStoryboard storyboardBackground)
{
if (storyboardUnloadDelegate?.Completed == false)
storyboardUnloadDelegate.Cancel();
else
storyboardBackground.LoadStoryboard();
storyboardUnloadDelegate = null;
}
base.OnResuming(e);
}
private ScheduledDelegate nextTask;
private CancellationTokenSource cancellationTokenSource;

View File

@ -60,7 +60,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
private bool canScaleX;
/// <summary>
/// Whether horizontal scaling support should be enabled.
/// Whether horizontal scaling (from the left or right edge) support should be enabled.
/// </summary>
public bool CanScaleX
{
@ -77,7 +77,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
private bool canScaleY;
/// <summary>
/// Whether vertical scaling support should be enabled.
/// Whether vertical scaling (from the top or bottom edge) support should be enabled.
/// </summary>
public bool CanScaleY
{
@ -91,6 +91,27 @@ namespace osu.Game.Screens.Edit.Compose.Components
}
}
private bool canScaleDiagonally;
/// <summary>
/// Whether diagonal scaling (from a corner) support should be enabled.
/// </summary>
/// <remarks>
/// There are some cases where we only want to allow proportional resizing, and not allow
/// one or both explicit directions of scale.
/// </remarks>
public bool CanScaleDiagonally
{
get => canScaleDiagonally;
set
{
if (canScaleDiagonally == value) return;
canScaleDiagonally = value;
recreate();
}
}
private bool canFlipX;
/// <summary>
@ -245,7 +266,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
};
if (CanScaleX) addXScaleComponents();
if (CanScaleX && CanScaleY) addFullScaleComponents();
if (CanScaleDiagonally) addFullScaleComponents();
if (CanScaleY) addYScaleComponents();
if (CanFlipX) addXFlipComponents();
if (CanFlipY) addYFlipComponents();

View File

@ -1095,6 +1095,19 @@ namespace osu.Game.Screens.Edit
protected void CreateNewDifficulty(RulesetInfo rulesetInfo)
{
if (isNewBeatmap)
{
dialogOverlay.Push(new SaveRequiredPopupDialog("This beatmap will be saved in order to create another difficulty.", () =>
{
if (!Save())
return;
CreateNewDifficulty(rulesetInfo);
}));
return;
}
if (!rulesetInfo.Equals(editorBeatmap.BeatmapInfo.Ruleset))
{
switchToNewDifficulty(rulesetInfo, false);

View File

@ -85,7 +85,8 @@ namespace osu.Game.Screens.Menu
private readonly List<MainMenuButton> buttonsTopLevel = new List<MainMenuButton>();
private readonly List<MainMenuButton> buttonsPlay = new List<MainMenuButton>();
private Sample sampleBack;
private Sample sampleBackToLogo;
private Sample sampleLogoSwoosh;
private readonly LogoTrackingContainer logoTrackingContainer;
@ -104,7 +105,7 @@ namespace osu.Game.Screens.Menu
buttonArea.AddRange(new Drawable[]
{
new MainMenuButton(ButtonSystemStrings.Settings, string.Empty, FontAwesome.Solid.Cog, new Color4(85, 85, 85, 255), () => OnSettings?.Invoke(), -WEDGE_WIDTH, Key.O),
backButton = new MainMenuButton(ButtonSystemStrings.Back, @"button-back-select", OsuIcon.LeftCircle, new Color4(51, 58, 94, 255), () => State = ButtonSystemState.TopLevel,
backButton = new MainMenuButton(ButtonSystemStrings.Back, @"back-to-top", OsuIcon.LeftCircle, new Color4(51, 58, 94, 255), () => State = ButtonSystemState.TopLevel,
-WEDGE_WIDTH)
{
VisibleState = ButtonSystemState.Play,
@ -127,14 +128,14 @@ namespace osu.Game.Screens.Menu
[BackgroundDependencyLoader(true)]
private void load(AudioManager audio, IdleTracker idleTracker, GameHost host)
{
buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Solo, @"button-solo-select", FontAwesome.Solid.User, new Color4(102, 68, 204, 255), () => OnSolo?.Invoke(), WEDGE_WIDTH, Key.P));
buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Multi, @"button-generic-select", FontAwesome.Solid.Users, new Color4(94, 63, 186, 255), onMultiplayer, 0, Key.M));
buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Playlists, @"button-generic-select", OsuIcon.Charts, new Color4(94, 63, 186, 255), onPlaylists, 0, Key.L));
buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Solo, @"button-default-select", FontAwesome.Solid.User, new Color4(102, 68, 204, 255), () => OnSolo?.Invoke(), WEDGE_WIDTH, Key.P));
buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Multi, @"button-default-select", FontAwesome.Solid.Users, new Color4(94, 63, 186, 255), onMultiplayer, 0, Key.M));
buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Playlists, @"button-default-select", OsuIcon.Charts, new Color4(94, 63, 186, 255), onPlaylists, 0, Key.L));
buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play);
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), () => State = ButtonSystemState.Play, WEDGE_WIDTH, Key.P));
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Edit, @"button-edit-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), () => OnEdit?.Invoke(), 0, Key.E));
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Browse, @"button-direct-select", OsuIcon.ChevronDownCircle, new Color4(165, 204, 0, 255), () => OnBeatmapListing?.Invoke(), 0, Key.B, Key.D));
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Edit, @"button-default-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), () => OnEdit?.Invoke(), 0, Key.E));
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Browse, @"button-default-select", OsuIcon.ChevronDownCircle, new Color4(165, 204, 0, 255), () => OnBeatmapListing?.Invoke(), 0, Key.B, Key.D));
if (host.CanExit)
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Exit, string.Empty, OsuIcon.CrossCircle, new Color4(238, 51, 153, 255), () => OnExit?.Invoke(), 0, Key.Q));
@ -155,7 +156,8 @@ namespace osu.Game.Screens.Menu
if (idleTracker != null) isIdle.BindTo(idleTracker.IsIdle);
sampleBack = audio.Samples.Get(@"Menu/button-back-select");
sampleBackToLogo = audio.Samples.Get(@"Menu/back-to-logo");
sampleLogoSwoosh = audio.Samples.Get(@"Menu/osu-logo-swoosh");
}
private void onMultiplayer()
@ -197,6 +199,7 @@ namespace osu.Game.Screens.Menu
{
if (State == ButtonSystemState.Initial)
{
StopSamplePlayback();
logo?.TriggerClick();
return true;
}
@ -260,10 +263,15 @@ namespace osu.Game.Screens.Menu
{
case ButtonSystemState.TopLevel:
State = ButtonSystemState.Initial;
sampleBack?.Play();
// Samples are explicitly played here in response to user interaction and not when transitioning due to idle.
StopSamplePlayback();
sampleBackToLogo?.Play();
return true;
case ButtonSystemState.Play:
StopSamplePlayback();
backButton.TriggerClick();
return true;
@ -272,6 +280,13 @@ namespace osu.Game.Screens.Menu
}
}
public void StopSamplePlayback()
{
buttonsPlay.ForEach(button => button.StopSamplePlayback());
buttonsTopLevel.ForEach(button => button.StopSamplePlayback());
logo?.StopSamplePlayback();
}
private bool onOsuLogo()
{
switch (state)
@ -346,6 +361,9 @@ namespace osu.Game.Screens.Menu
logo?.MoveTo(new Vector2(0.5f), 800, Easing.OutExpo);
logo?.ScaleTo(1, 800, Easing.OutExpo);
}, buttonArea.Alpha * 150);
if (lastState == ButtonSystemState.TopLevel)
sampleLogoSwoosh?.Play();
break;
case ButtonSystemState.TopLevel:

View File

@ -7,6 +7,8 @@ using System;
using System.Diagnostics;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -89,8 +91,10 @@ namespace osu.Game.Screens.Menu
private SongTicker songTicker;
private Container logoTarget;
private Sample reappearSampleSwoosh;
[BackgroundDependencyLoader(true)]
private void load(BeatmapListingOverlay beatmapListing, SettingsOverlay settings, OsuConfigManager config, SessionStatics statics)
private void load(BeatmapListingOverlay beatmapListing, SettingsOverlay settings, OsuConfigManager config, SessionStatics statics, AudioManager audio)
{
holdDelay = config.GetBindable<double>(OsuSetting.UIHoldActivationDelay);
loginDisplayed = statics.GetBindable<bool>(Static.LoginOverlayDisplayed);
@ -162,6 +166,8 @@ namespace osu.Game.Screens.Menu
Buttons.OnSettings = () => settings?.ToggleVisibility();
Buttons.OnBeatmapListing = () => beatmapListing?.ToggleVisibility();
reappearSampleSwoosh = audio.Samples.Get(@"Menu/reappear-swoosh");
preloadSongSelect();
}
@ -291,6 +297,10 @@ namespace osu.Game.Screens.Menu
{
base.OnResuming(e);
// Ensures any playing `ButtonSystem` samples are stopped when returning to MainMenu (as to not overlap with the 'back' sample)
Buttons.StopSamplePlayback();
reappearSampleSwoosh?.Play();
ApplyToBackground(b => (b as BackgroundScreenDefault)?.Next());
// we may have consumed our preloaded instance, so let's make another.

View File

@ -51,6 +51,7 @@ namespace osu.Game.Screens.Menu
private readonly Action clickAction;
private Sample sampleClick;
private Sample sampleHover;
private SampleChannel sampleChannel;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => box.ReceivePositionalInputAt(screenSpacePos);
@ -225,7 +226,8 @@ namespace osu.Game.Screens.Menu
private void trigger()
{
sampleClick?.Play();
sampleChannel = sampleClick?.GetChannel();
sampleChannel?.Play();
clickAction?.Invoke();
@ -237,6 +239,8 @@ namespace osu.Game.Screens.Menu
public override bool HandleNonPositionalInput => state == ButtonState.Expanded;
public override bool HandlePositionalInput => state != ButtonState.Exploded && box.Scale.X >= 0.8f;
public void StopSamplePlayback() => sampleChannel?.Stop();
protected override void Update()
{
iconText.Alpha = Math.Clamp((box.Scale.X - 0.5f) / 0.3f, 0, 1);

View File

@ -52,6 +52,8 @@ namespace osu.Game.Screens.Menu
private readonly IntroSequence intro;
private Sample sampleClick;
private SampleChannel sampleClickChannel;
private Sample sampleBeat;
private Sample sampleDownbeat;
@ -391,7 +393,11 @@ namespace osu.Game.Screens.Menu
flashLayer.FadeOut(1500, Easing.OutExpo);
if (Action?.Invoke() == true)
sampleClick.Play();
{
StopSamplePlayback();
sampleClickChannel = sampleClick.GetChannel();
sampleClickChannel.Play();
}
return true;
}
@ -440,6 +446,8 @@ namespace osu.Game.Screens.Menu
private Container currentProxyTarget;
private Drawable proxy;
public void StopSamplePlayback() => sampleClickChannel?.Stop();
public Drawable ProxyToContainer(Container c)
{
if (currentProxyTarget != null)

View File

@ -115,7 +115,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex(@"27252d"),
},
avatar = new UpdateableAvatar(showUsernameTooltip: true) { RelativeSizeAxes = Axes.Both },
avatar = new UpdateableAvatar(showUserPanelOnHover: true) { RelativeSizeAxes = Axes.Both },
};
}
}

View File

@ -289,7 +289,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
set => avatar.User = value;
}
private readonly UpdateableAvatar avatar = new UpdateableAvatar(showUsernameTooltip: true) { RelativeSizeAxes = Axes.Both };
private readonly UpdateableAvatar avatar = new UpdateableAvatar(showUserPanelOnHover: true) { RelativeSizeAxes = Axes.Both };
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colours)

View File

@ -10,8 +10,6 @@ namespace osu.Game.Screens.Play
{
public partial class ArgonKeyCounterDisplay : KeyCounterDisplay
{
private const int duration = 100;
protected override FillFlowContainer<KeyCounter> KeyFlow { get; }
public ArgonKeyCounterDisplay()
@ -25,16 +23,6 @@ namespace osu.Game.Screens.Play
};
}
protected override void Update()
{
base.Update();
Size = KeyFlow.Size;
}
protected override KeyCounter CreateCounter(InputTrigger trigger) => new ArgonKeyCounter(trigger);
protected override void UpdateVisibility()
=> KeyFlow.FadeTo(AlwaysVisible.Value || ConfigVisibility.Value ? 1 : 0, duration);
}
}

View File

@ -0,0 +1,110 @@
// 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.Bindables;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Localisation.SkinComponents;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Screens.Play.HUD
{
public partial class ArgonAccuracyCounter : GameplayAccuracyCounter, ISerialisableDrawable
{
protected override double RollingDuration => 500;
protected override Easing RollingEasing => Easing.OutQuint;
[SettingSource("Wireframe opacity", "Controls the opacity of the wire frames behind the digits.")]
public BindableFloat WireframeOpacity { get; } = new BindableFloat(0.25f)
{
Precision = 0.01f,
MinValue = 0,
MaxValue = 1,
};
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.ShowLabel), nameof(SkinnableComponentStrings.ShowLabelDescription))]
public Bindable<bool> ShowLabel { get; } = new BindableBool(true);
public bool UsesFixedAnchor { get; set; }
protected override IHasText CreateText() => new ArgonAccuracyTextComponent
{
WireframeOpacity = { BindTarget = WireframeOpacity },
ShowLabel = { BindTarget = ShowLabel },
};
private partial class ArgonAccuracyTextComponent : CompositeDrawable, IHasText
{
private readonly ArgonCounterTextComponent wholePart;
private readonly ArgonCounterTextComponent fractionPart;
private readonly ArgonCounterTextComponent percentText;
public IBindable<float> WireframeOpacity { get; } = new BindableFloat();
public Bindable<bool> ShowLabel { get; } = new BindableBool();
public LocalisableString Text
{
get => wholePart.Text;
set
{
string[] split = value.ToString().Replace("%", string.Empty).Split(".");
wholePart.Text = split[0];
fractionPart.Text = "." + split[1];
}
}
public ArgonAccuracyTextComponent()
{
AutoSizeAxes = Axes.Both;
InternalChild = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
new Container
{
AutoSizeAxes = Axes.Both,
Child = wholePart = new ArgonCounterTextComponent(Anchor.TopRight, BeatmapsetsStrings.ShowScoreboardHeadersAccuracy.ToUpper())
{
RequiredDisplayDigits = { Value = 3 },
WireframeOpacity = { BindTarget = WireframeOpacity },
ShowLabel = { BindTarget = ShowLabel },
}
},
fractionPart = new ArgonCounterTextComponent(Anchor.TopLeft)
{
WireframeOpacity = { BindTarget = WireframeOpacity },
Scale = new Vector2(0.5f),
},
percentText = new ArgonCounterTextComponent(Anchor.TopLeft)
{
Text = @"%",
WireframeOpacity = { BindTarget = WireframeOpacity }
},
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
ShowLabel.BindValueChanged(s =>
{
fractionPart.Margin = new MarginPadding { Top = s.NewValue ? 12f * 2f + 4f : 4f }; // +4 to account for the extra spaces above the digits.
percentText.Margin = new MarginPadding { Top = s.NewValue ? 12f : 0 };
}, true);
}
}
}
}

View File

@ -0,0 +1,68 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Localisation.SkinComponents;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets.Scoring;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Play.HUD
{
public partial class ArgonComboCounter : ComboCounter
{
private ArgonCounterTextComponent text = null!;
protected override double RollingDuration => 500;
protected override Easing RollingEasing => Easing.OutQuint;
[SettingSource("Wireframe opacity", "Controls the opacity of the wire frames behind the digits.")]
public BindableFloat WireframeOpacity { get; } = new BindableFloat(0.25f)
{
Precision = 0.01f,
MinValue = 0,
MaxValue = 1,
};
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.ShowLabel), nameof(SkinnableComponentStrings.ShowLabelDescription))]
public Bindable<bool> ShowLabel { get; } = new BindableBool(true);
[BackgroundDependencyLoader]
private void load(ScoreProcessor scoreProcessor)
{
Current.BindTo(scoreProcessor.Combo);
Current.BindValueChanged(combo =>
{
bool wasIncrease = combo.NewValue > combo.OldValue;
bool wasMiss = combo.OldValue > 1 && combo.NewValue == 0;
float newScale = Math.Clamp(text.NumberContainer.Scale.X * (wasIncrease ? 1.1f : 0.8f), 0.6f, 1.4f);
float duration = wasMiss ? 2000 : 500;
text.NumberContainer
.ScaleTo(new Vector2(newScale))
.ScaleTo(Vector2.One, duration, Easing.OutQuint);
if (wasMiss)
text.FlashColour(Color4.Red, duration, Easing.OutQuint);
});
}
protected override LocalisableString FormatCount(int count) => $@"{count}x";
protected override IHasText CreateText() => text = new ArgonCounterTextComponent(Anchor.TopLeft, MatchesStrings.MatchScoreStatsCombo.ToUpper())
{
WireframeOpacity = { BindTarget = WireframeOpacity },
ShowLabel = { BindTarget = ShowLabel },
};
}
}

View File

@ -0,0 +1,173 @@
// 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.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Localisation;
using osu.Framework.Text;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK;
namespace osu.Game.Screens.Play.HUD
{
public partial class ArgonCounterTextComponent : CompositeDrawable, IHasText
{
private readonly ArgonCounterSpriteText wireframesPart;
private readonly ArgonCounterSpriteText textPart;
private readonly OsuSpriteText labelText;
public IBindable<float> WireframeOpacity { get; } = new BindableFloat();
public Bindable<int> RequiredDisplayDigits { get; } = new BindableInt();
public Bindable<bool> ShowLabel { get; } = new BindableBool();
public Container NumberContainer { get; private set; }
public LocalisableString Text
{
get => textPart.Text;
set
{
int remainingCount = RequiredDisplayDigits.Value - value.ToString().Count(char.IsDigit);
string remainingText = remainingCount > 0 ? new string('#', remainingCount) : string.Empty;
wireframesPart.Text = remainingText + value;
textPart.Text = value;
}
}
public ArgonCounterTextComponent(Anchor anchor, LocalisableString? label = null)
{
Anchor = anchor;
Origin = anchor;
AutoSizeAxes = Axes.Both;
InternalChild = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
labelText = new OsuSpriteText
{
Alpha = 0,
Text = label.GetValueOrDefault(),
Font = OsuFont.Torus.With(size: 12, weight: FontWeight.Bold),
Margin = new MarginPadding { Left = 2.5f },
},
NumberContainer = new Container
{
AutoSizeAxes = Axes.Both,
Children = new[]
{
wireframesPart = new ArgonCounterSpriteText(wireframesLookup)
{
Anchor = anchor,
Origin = anchor,
},
textPart = new ArgonCounterSpriteText(textLookup)
{
Anchor = anchor,
Origin = anchor,
},
}
}
}
};
}
private string textLookup(char c)
{
switch (c)
{
case '.':
return @"dot";
case '%':
return @"percentage";
default:
return c.ToString();
}
}
private string wireframesLookup(char c)
{
if (c == '.') return @"dot";
return @"wireframes";
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
labelText.Colour = colours.Blue0;
}
protected override void LoadComplete()
{
base.LoadComplete();
WireframeOpacity.BindValueChanged(v => wireframesPart.Alpha = v.NewValue, true);
ShowLabel.BindValueChanged(s => labelText.Alpha = s.NewValue ? 1 : 0, true);
}
private partial class ArgonCounterSpriteText : OsuSpriteText
{
private readonly Func<char, string> getLookup;
private GlyphStore glyphStore = null!;
protected override char FixedWidthReferenceCharacter => '5';
public ArgonCounterSpriteText(Func<char, string> getLookup)
{
this.getLookup = getLookup;
Shadow = false;
UseFullGlyphHeight = false;
}
[BackgroundDependencyLoader]
private void load(TextureStore textures)
{
Spacing = new Vector2(-2f, 0f);
Font = new FontUsage(@"argon-counter", 1);
glyphStore = new GlyphStore(textures, getLookup);
}
protected override TextBuilder CreateTextBuilder(ITexturedGlyphLookupStore store) => base.CreateTextBuilder(glyphStore);
private class GlyphStore : ITexturedGlyphLookupStore
{
private readonly TextureStore textures;
private readonly Func<char, string> getLookup;
public GlyphStore(TextureStore textures, Func<char, string> getLookup)
{
this.textures = textures;
this.getLookup = getLookup;
}
public ITexturedCharacterGlyph? Get(string fontName, char character)
{
string lookup = getLookup(character);
var texture = textures.Get($"Gameplay/Fonts/{fontName}-{lookup}");
if (texture == null)
return null;
return new TexturedCharacterGlyph(new CharacterGlyph(character, 0, 0, texture.Width, texture.Height, null), texture, 0.125f);
}
public Task<ITexturedCharacterGlyph?> GetAsync(string fontName, char character) => Task.Run(() => Get(fontName, character));
}
}
}
}

View File

@ -35,13 +35,8 @@ namespace osu.Game.Screens.Play.HUD
Precision = 1
};
[SettingSource("Bar length")]
public BindableFloat BarLength { get; } = new BindableFloat(0.98f)
{
MinValue = 0.2f,
MaxValue = 1,
Precision = 0.01f,
};
[SettingSource("Use relative size")]
public BindableBool UseRelativeSize { get; } = new BindableBool(true);
private BarPath mainBar = null!;
@ -92,12 +87,30 @@ namespace osu.Game.Screens.Play.HUD
}
}
private const float main_path_radius = 10f;
public const float MAIN_PATH_RADIUS = 10f;
private const float curve_start_offset = 70;
private const float curve_end_offset = 40;
private const float padding = MAIN_PATH_RADIUS * 2;
private const float curve_smoothness = 10;
private readonly LayoutValue drawSizeLayout = new LayoutValue(Invalidation.DrawSize);
public ArgonHealthDisplay()
{
AddLayout(drawSizeLayout);
// sane default width specification.
// this only matters if the health display isn't part of the default skin
// (in which case width will be set to 300 via `ArgonSkin.GetDrawableComponent()`),
// and if the user hasn't applied their own modifications
// (which are applied via `SerialisedDrawableInfo.ApplySerialisedInfo()`).
Width = 0.98f;
}
[BackgroundDependencyLoader]
private void load()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChild = new Container
@ -107,7 +120,7 @@ namespace osu.Game.Screens.Play.HUD
{
background = new BackgroundPath
{
PathRadius = main_path_radius,
PathRadius = MAIN_PATH_RADIUS,
},
glowBar = new BarPath
{
@ -127,7 +140,7 @@ namespace osu.Game.Screens.Play.HUD
Blending = BlendingParameters.Additive,
BarColour = main_bar_colour,
GlowColour = main_bar_glow_colour,
PathRadius = main_path_radius,
PathRadius = MAIN_PATH_RADIUS,
GlowPortion = 0.6f,
},
}
@ -140,17 +153,15 @@ namespace osu.Game.Screens.Play.HUD
Current.BindValueChanged(_ => Scheduler.AddOnce(updateCurrent), true);
BarLength.BindValueChanged(l => Width = l.NewValue, true);
BarHeight.BindValueChanged(_ => updatePath());
updatePath();
}
// we're about to set `RelativeSizeAxes` depending on the value of `UseRelativeSize`.
// setting `RelativeSizeAxes` internally transforms absolute sizing to relative and back to keep the size the same,
// but that is not what we want in this case, since the width at this point is valid in the *target* sizing mode.
// to counteract this, store the numerical value here, and restore it after setting the correct initial relative sizing axes.
float previousWidth = Width;
UseRelativeSize.BindValueChanged(v => RelativeSizeAxes = v.NewValue ? Axes.X : Axes.None, true);
Width = previousWidth;
protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source)
{
if ((invalidation & Invalidation.DrawSize) > 0)
updatePath();
return base.OnInvalidate(invalidation, source);
BarHeight.BindValueChanged(_ => updatePath(), true);
}
private void updateCurrent()
@ -168,6 +179,12 @@ namespace osu.Game.Screens.Play.HUD
{
base.Update();
if (!drawSizeLayout.IsValid)
{
updatePath();
drawSizeLayout.Validate();
}
mainBar.Alpha = (float)Interpolation.DampContinuously(mainBar.Alpha, Current.Value > 0 ? 1 : 0, 40, Time.Elapsed);
glowBar.Alpha = (float)Interpolation.DampContinuously(glowBar.Alpha, GlowBarValue > 0 ? 1 : 0, 40, Time.Elapsed);
}
@ -236,11 +253,17 @@ namespace osu.Game.Screens.Play.HUD
private void updatePath()
{
float barLength = DrawWidth - main_path_radius * 2;
float curveStart = barLength - 70;
float curveEnd = barLength - 40;
float usableWidth = DrawWidth - padding;
const float curve_smoothness = 10;
if (usableWidth < 0) enforceMinimumWidth();
// the display starts curving at `curve_start_offset` units from the right and ends curving at `curve_end_offset`.
// to ensure that the curve is symmetric when it starts being narrow enough, add a `curve_end_offset` to the left side too.
const float rescale_cutoff = curve_start_offset + curve_end_offset;
float barLength = Math.Max(DrawWidth - padding, rescale_cutoff);
float curveStart = barLength - curve_start_offset;
float curveEnd = barLength - curve_end_offset;
Vector2 diagonalDir = (new Vector2(curveEnd, BarHeight.Value) - new Vector2(curveStart, 0)).Normalized();
@ -256,6 +279,9 @@ namespace osu.Game.Screens.Play.HUD
new PathControlPoint(new Vector2(barLength, BarHeight.Value)),
});
if (DrawWidth - padding < rescale_cutoff)
rescalePathProportionally();
List<Vector2> vertices = new List<Vector2>();
barPath.GetPathToProgress(vertices, 0.0, 1.0);
@ -264,6 +290,24 @@ namespace osu.Game.Screens.Play.HUD
glowBar.Vertices = vertices;
updatePathVertices();
void enforceMinimumWidth()
{
// Switch to absolute in order to be able to define a minimum width.
// Then switch back is required. Framework will handle the conversion for us.
Axes relativeAxes = RelativeSizeAxes;
RelativeSizeAxes = Axes.None;
Width = padding;
RelativeSizeAxes = relativeAxes;
}
void rescalePathProportionally()
{
foreach (var point in barPath.ControlPoints)
point.Position = new Vector2(point.Position.X / barLength * (DrawWidth - padding), point.Position.Y);
}
}
private void updatePathVertices()

View File

@ -0,0 +1,51 @@
// 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.Bindables;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Localisation.SkinComponents;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Skinning;
namespace osu.Game.Screens.Play.HUD
{
public partial class ArgonScoreCounter : GameplayScoreCounter, ISerialisableDrawable
{
protected override double RollingDuration => 500;
protected override Easing RollingEasing => Easing.OutQuint;
[SettingSource("Wireframe opacity", "Controls the opacity of the wire frames behind the digits.")]
public BindableFloat WireframeOpacity { get; } = new BindableFloat(0.25f)
{
Precision = 0.01f,
MinValue = 0,
MaxValue = 1,
};
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.ShowLabel), nameof(SkinnableComponentStrings.ShowLabelDescription))]
public Bindable<bool> ShowLabel { get; } = new BindableBool(true);
public bool UsesFixedAnchor { get; set; }
protected override LocalisableString FormatCount(long count) => count.ToLocalisableString();
protected override IHasText CreateText() => new ArgonScoreTextComponent(Anchor.TopRight, BeatmapsetsStrings.ShowScoreboardHeadersScore.ToUpper())
{
RequiredDisplayDigits = { BindTarget = RequiredDisplayDigits },
WireframeOpacity = { BindTarget = WireframeOpacity },
ShowLabel = { BindTarget = ShowLabel },
};
private partial class ArgonScoreTextComponent : ArgonCounterTextComponent
{
public ArgonScoreTextComponent(Anchor anchor, LocalisableString? label = null)
: base(anchor, label)
{
}
}
}
}

View File

@ -19,6 +19,7 @@ namespace osu.Game.Screens.Play.HUD
private readonly ArgonSongProgressGraph graph;
private readonly ArgonSongProgressBar bar;
private readonly Container graphContainer;
private readonly Container content;
private const float bar_height = 10;
@ -30,43 +31,50 @@ namespace osu.Game.Screens.Play.HUD
public ArgonSongProgress()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Anchor = Anchor.BottomCentre;
Origin = Anchor.BottomCentre;
Masking = true;
CornerRadius = 5;
Children = new Drawable[]
Child = content = new Container
{
info = new SongProgressInfo
RelativeSizeAxes = Axes.X,
Children = new Drawable[]
{
Origin = Anchor.TopLeft,
Name = "Info",
Anchor = Anchor.TopLeft,
RelativeSizeAxes = Axes.X,
ShowProgress = false
},
bar = new ArgonSongProgressBar(bar_height)
{
Name = "Seek bar",
Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft,
OnSeek = time => player?.Seek(time),
},
graphContainer = new Container
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Masking = true,
CornerRadius = 5,
Child = graph = new ArgonSongProgressGraph
info = new SongProgressInfo
{
Name = "Difficulty graph",
RelativeSizeAxes = Axes.Both,
Blending = BlendingParameters.Additive
Origin = Anchor.TopLeft,
Name = "Info",
Anchor = Anchor.TopLeft,
RelativeSizeAxes = Axes.X,
ShowProgress = false
},
RelativeSizeAxes = Axes.X,
},
bar = new ArgonSongProgressBar(bar_height)
{
Name = "Seek bar",
Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft,
OnSeek = time => player?.Seek(time),
},
graphContainer = new Container
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Masking = true,
CornerRadius = 5,
Child = graph = new ArgonSongProgressGraph
{
Name = "Difficulty graph",
RelativeSizeAxes = Axes.Both,
Blending = BlendingParameters.Additive
},
RelativeSizeAxes = Axes.X,
},
}
};
RelativeSizeAxes = Axes.X;
}
[BackgroundDependencyLoader]
@ -100,7 +108,7 @@ namespace osu.Game.Screens.Play.HUD
protected override void Update()
{
base.Update();
Height = bar.Height + bar_height + info.Height;
content.Height = bar.Height + bar_height + info.Height;
graphContainer.Height = bar.Height;
}

View File

@ -0,0 +1,51 @@
// 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.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Configuration;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Screens.Play.HUD
{
public partial class ArgonWedgePiece : CompositeDrawable, ISerialisableDrawable
{
public bool UsesFixedAnchor { get; set; }
[SettingSource("Inverted shear")]
public BindableBool InvertShear { get; } = new BindableBool();
public ArgonWedgePiece()
{
CornerRadius = 10f;
Size = new Vector2(400, 100);
}
[BackgroundDependencyLoader]
private void load()
{
Masking = true;
Shear = new Vector2(0.8f, 0f);
InternalChild = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientVertical(Color4Extensions.FromHex("#66CCFF").Opacity(0.0f), Color4Extensions.FromHex("#66CCFF").Opacity(0.25f)),
};
}
protected override void LoadComplete()
{
base.LoadComplete();
InvertShear.BindValueChanged(v => Shear = new Vector2(0.8f, 0f) * (v.NewValue ? -1 : 1), true);
}
}
}

View File

@ -10,7 +10,6 @@ namespace osu.Game.Screens.Play.HUD
{
public partial class DefaultKeyCounterDisplay : KeyCounterDisplay
{
private const int duration = 100;
private const double key_fade_time = 80;
protected override FillFlowContainer<KeyCounter> KeyFlow { get; }
@ -25,15 +24,6 @@ namespace osu.Game.Screens.Play.HUD
};
}
protected override void Update()
{
base.Update();
// Don't use autosize as it will shrink to zero when KeyFlow is hidden.
// In turn this can cause the display to be masked off screen and never become visible again.
Size = KeyFlow.Size;
}
protected override KeyCounter CreateCounter(InputTrigger trigger) => new DefaultKeyCounter(trigger)
{
FadeTime = key_fade_time,
@ -41,10 +31,6 @@ namespace osu.Game.Screens.Play.HUD
KeyUpTextColor = KeyUpTextColor,
};
protected override void UpdateVisibility() =>
// Isolate changing visibility of the key counters from fading this component.
KeyFlow.FadeTo(AlwaysVisible.Value || ConfigVisibility.Value ? 1 : 0, duration);
private Color4 keyDownTextColor = Color4.DarkGray;
public Color4 KeyDownTextColor

View File

@ -5,6 +5,7 @@ using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Graphics;
@ -27,6 +28,7 @@ namespace osu.Game.Screens.Play.HUD
private readonly DefaultSongProgressBar bar;
private readonly DefaultSongProgressGraph graph;
private readonly SongProgressInfo info;
private readonly Container content;
[SettingSource(typeof(SongProgressStrings), nameof(SongProgressStrings.ShowGraph), nameof(SongProgressStrings.ShowGraphDescription))]
public Bindable<bool> ShowGraph { get; } = new BindableBool(true);
@ -37,31 +39,36 @@ namespace osu.Game.Screens.Play.HUD
public DefaultSongProgress()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Anchor = Anchor.BottomRight;
Origin = Anchor.BottomRight;
Children = new Drawable[]
Child = content = new Container
{
info = new SongProgressInfo
RelativeSizeAxes = Axes.X,
Children = new Drawable[]
{
Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft,
RelativeSizeAxes = Axes.X,
},
graph = new DefaultSongProgressGraph
{
RelativeSizeAxes = Axes.X,
Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft,
Height = graph_height,
Margin = new MarginPadding { Bottom = bottom_bar_height },
},
bar = new DefaultSongProgressBar(bottom_bar_height, graph_height, handle_size)
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
OnSeek = time => player?.Seek(time),
},
info = new SongProgressInfo
{
Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft,
RelativeSizeAxes = Axes.X,
},
graph = new DefaultSongProgressGraph
{
RelativeSizeAxes = Axes.X,
Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft,
Height = graph_height,
Margin = new MarginPadding { Bottom = bottom_bar_height },
},
bar = new DefaultSongProgressBar(bottom_bar_height, graph_height, handle_size)
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
OnSeek = time => player?.Seek(time),
},
}
};
}
@ -107,7 +114,7 @@ namespace osu.Game.Screens.Play.HUD
float newHeight = bottom_bar_height + graph_height + handle_size.Y + info.Height - graph.Y;
if (!Precision.AlmostEquals(Height, newHeight, 5f))
Height = newHeight;
content.Height = newHeight;
}
private void updateBarVisibility()

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