mirror of
https://github.com/ppy/osu.git
synced 2025-01-12 11:42:54 +08:00
Merge branch 'master' into hp-drain-v1-2
This commit is contained in:
commit
6fa7b4f552
4
.github/workflows/diffcalc.yml
vendored
4
.github/workflows/diffcalc.yml
vendored
@ -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
|
||||
|
@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.1030.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.1111.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
204
osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs
Normal file
204
osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs
Normal 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,
|
||||
})
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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());
|
||||
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
@ -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[]
|
||||
|
BIN
osu.Game.Tests/Resources/Archives/modified-argon-20231108.osk
Normal file
BIN
osu.Game.Tests/Resources/Archives/modified-argon-20231108.osk
Normal file
Binary file not shown.
@ -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)
|
||||
|
@ -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 =>
|
||||
{
|
||||
|
@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -47,6 +47,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
CanScaleX = true,
|
||||
CanScaleY = true,
|
||||
CanScaleDiagonally = true,
|
||||
CanFlipX = true,
|
||||
CanFlipY = true,
|
||||
|
||||
|
@ -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]
|
||||
|
@ -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())));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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) };
|
||||
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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[]
|
||||
{
|
||||
|
86
osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs
Normal file
86
osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs
Normal 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),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
};
|
||||
|
@ -203,6 +203,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
|
||||
public IBeatmap Beatmap { get; }
|
||||
|
||||
// ReSharper disable once NotNullOrRequiredMemberIsNotInitialized
|
||||
public TestBeatmapConverter(IBeatmap beatmap)
|
||||
{
|
||||
Beatmap = beatmap;
|
||||
|
@ -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", () =>
|
||||
{
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
||||
|
72
osu.Game/Input/TouchInputInterceptor.cs
Normal file
72
osu.Game/Input/TouchInputInterceptor.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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'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}";
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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),
|
||||
},
|
||||
|
@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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)!
|
||||
}));
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Input.Handlers;
|
||||
using osu.Framework.Localisation;
|
||||
@ -28,11 +29,14 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager osuConfig)
|
||||
{
|
||||
Add(new SettingsCheckbox
|
||||
if (!RuntimeInfo.IsMobile) // don't allow disabling the only input method (touch) on mobile.
|
||||
{
|
||||
LabelText = CommonStrings.Enabled,
|
||||
Current = handler.Enabled
|
||||
});
|
||||
Add(new SettingsCheckbox
|
||||
{
|
||||
LabelText = CommonStrings.Enabled,
|
||||
Current = handler.Enabled
|
||||
});
|
||||
}
|
||||
|
||||
Add(new SettingsCheckbox
|
||||
{
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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 });
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
||||
|
24
osu.Game/Rulesets/Mods/ModTouchDevice.cs
Normal file
24
osu.Game/Rulesets/Mods/ModTouchDevice.cs
Normal 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) };
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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.");
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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 },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
110
osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs
Normal file
110
osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
68
osu.Game/Screens/Play/HUD/ArgonComboCounter.cs
Normal file
68
osu.Game/Screens/Play/HUD/ArgonComboCounter.cs
Normal 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 },
|
||||
};
|
||||
}
|
||||
}
|
173
osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs
Normal file
173
osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
51
osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs
Normal file
51
osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
|
51
osu.Game/Screens/Play/HUD/ArgonWedgePiece.cs
Normal file
51
osu.Game/Screens/Play/HUD/ArgonWedgePiece.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -4,6 +4,7 @@
|
||||
using System.Collections.Specialized;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.UI;
|
||||
@ -31,13 +32,27 @@ namespace osu.Game.Screens.Play.HUD
|
||||
[Resolved]
|
||||
private InputCountController controller { get; set; } = null!;
|
||||
|
||||
protected abstract void UpdateVisibility();
|
||||
private const int duration = 100;
|
||||
|
||||
protected void UpdateVisibility()
|
||||
{
|
||||
bool visible = AlwaysVisible.Value || ConfigVisibility.Value;
|
||||
|
||||
// Isolate changing visibility of the key counters from fading this component.
|
||||
KeyFlow.FadeTo(visible ? 1 : 0, duration);
|
||||
|
||||
// Ensure a valid size is immediately obtained even if partially off-screen
|
||||
// See https://github.com/ppy/osu/issues/14793.
|
||||
KeyFlow.AlwaysPresent = visible;
|
||||
}
|
||||
|
||||
protected abstract KeyCounter CreateCounter(InputTrigger trigger);
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config, DrawableRuleset? drawableRuleset)
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
config.BindWith(OsuSetting.KeyOverlay, ConfigVisibility);
|
||||
|
||||
if (drawableRuleset != null)
|
||||
|
@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Configuration;
|
||||
@ -22,8 +23,9 @@ namespace osu.Game.Screens.Play.PlayerSettings
|
||||
{
|
||||
new PlayerCheckbox
|
||||
{
|
||||
LabelText = MouseSettingsStrings.DisableClicksDuringGameplay,
|
||||
Current = config.GetBindable<bool>(OsuSetting.MouseDisableButtons)
|
||||
// TODO: change to touchscreen detection once https://github.com/ppy/osu/pull/25348 makes it in
|
||||
LabelText = RuntimeInfo.IsDesktop ? MouseSettingsStrings.DisableClicksDuringGameplay : TouchSettingsStrings.DisableTapsDuringGameplay,
|
||||
Current = config.GetBindable<bool>(RuntimeInfo.IsDesktop ? OsuSetting.MouseDisableButtons : OsuSetting.TouchDisableGameplayTaps)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
60
osu.Game/Screens/Play/PlayerTouchInputDetector.cs
Normal file
60
osu.Game/Screens/Play/PlayerTouchInputDetector.cs
Normal file
@ -0,0 +1,60 @@
|
||||
// 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 osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Screens.Play
|
||||
{
|
||||
public partial class PlayerTouchInputDetector : Component
|
||||
{
|
||||
[Resolved]
|
||||
private Player player { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private GameplayState gameplayState { get; set; } = null!;
|
||||
|
||||
private IBindable<bool> touchActive = new BindableBool();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(SessionStatics statics)
|
||||
{
|
||||
touchActive = statics.GetBindable<bool>(Static.TouchInputActive);
|
||||
touchActive.BindValueChanged(_ => updateState());
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
if (!touchActive.Value)
|
||||
return;
|
||||
|
||||
if (gameplayState.HasPassed || gameplayState.HasFailed || gameplayState.HasQuit)
|
||||
return;
|
||||
|
||||
if (gameplayState.Score.ScoreInfo.Mods.OfType<ModTouchDevice>().Any())
|
||||
return;
|
||||
|
||||
if (player.IsBreakTime.Value)
|
||||
return;
|
||||
|
||||
var touchDeviceMod = gameplayState.Ruleset.GetTouchDeviceMod();
|
||||
if (touchDeviceMod == null)
|
||||
return;
|
||||
|
||||
var candidateMods = player.Score.ScoreInfo.Mods.Append(touchDeviceMod).ToArray();
|
||||
|
||||
if (!ModUtils.CheckCompatibleSet(candidateMods, out _))
|
||||
return;
|
||||
|
||||
// `Player` (probably rightly so) assumes immutability of mods,
|
||||
// so this will not be shown immediately on the mod display in the top right.
|
||||
// if this is to change, the mod immutability should be revisited.
|
||||
player.Score.ScoreInfo.Mods = candidateMods;
|
||||
}
|
||||
}
|
||||
}
|
@ -44,6 +44,18 @@ namespace osu.Game.Screens.Play
|
||||
{
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
if (DrawableRuleset == null)
|
||||
{
|
||||
// base load must have failed (e.g. due to an unknown mod); bail.
|
||||
return;
|
||||
}
|
||||
|
||||
AddInternal(new PlayerTouchInputDetector());
|
||||
}
|
||||
|
||||
protected override void LoadAsyncComplete()
|
||||
{
|
||||
base.LoadAsyncComplete();
|
||||
|
@ -48,6 +48,8 @@ namespace osu.Game.Screens.Select
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
BeatmapOptions.AddButton(ButtonSystemStrings.Edit.ToSentence(), @"beatmap", FontAwesome.Solid.PencilAlt, colours.Yellow, () => Edit());
|
||||
|
||||
AddInternal(new SongSelectTouchInputDetector());
|
||||
}
|
||||
|
||||
protected void PresentScore(ScoreInfo score) =>
|
||||
|
69
osu.Game/Screens/Select/SongSelectTouchInputDetector.cs
Normal file
69
osu.Game/Screens/Select/SongSelectTouchInputDetector.cs
Normal file
@ -0,0 +1,69 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Screens.Select
|
||||
{
|
||||
public partial class SongSelectTouchInputDetector : Component
|
||||
{
|
||||
[Resolved]
|
||||
private Bindable<RulesetInfo> ruleset { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private Bindable<IReadOnlyList<Mod>> mods { get; set; } = null!;
|
||||
|
||||
private IBindable<bool> touchActive = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(SessionStatics statics)
|
||||
{
|
||||
touchActive = statics.GetBindable<bool>(Static.TouchInputActive);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
ruleset.BindValueChanged(_ => Scheduler.AddOnce(updateState));
|
||||
mods.BindValueChanged(_ => Scheduler.AddOnce(updateState));
|
||||
mods.BindDisabledChanged(_ => Scheduler.AddOnce(updateState));
|
||||
touchActive.BindValueChanged(_ => Scheduler.AddOnce(updateState));
|
||||
updateState();
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
if (mods.Disabled)
|
||||
return;
|
||||
|
||||
var touchDeviceMod = ruleset.Value.CreateInstance().GetTouchDeviceMod();
|
||||
|
||||
if (touchDeviceMod == null)
|
||||
return;
|
||||
|
||||
bool touchDeviceModEnabled = mods.Value.Any(mod => mod is ModTouchDevice);
|
||||
|
||||
if (touchActive.Value && !touchDeviceModEnabled)
|
||||
{
|
||||
var candidateMods = mods.Value.Append(touchDeviceMod).ToArray();
|
||||
|
||||
if (!ModUtils.CheckCompatibleSet(candidateMods, out _))
|
||||
return;
|
||||
|
||||
mods.Value = candidateMods;
|
||||
}
|
||||
|
||||
if (!touchActive.Value && touchDeviceModEnabled)
|
||||
mods.Value = mods.Value.Where(mod => mod is not ModTouchDevice).ToArray();
|
||||
}
|
||||
}
|
||||
}
|
@ -15,6 +15,7 @@ using osu.Game.IO;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Screens.Play.HUD.HitErrorMeters;
|
||||
using osu.Game.Skinning.Components;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@ -40,7 +41,10 @@ namespace osu.Game.Skinning
|
||||
|
||||
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)]
|
||||
public ArgonSkin(SkinInfo skin, IStorageResourceProvider resources)
|
||||
: base(skin, resources)
|
||||
: base(
|
||||
skin,
|
||||
resources
|
||||
)
|
||||
{
|
||||
Resources = resources;
|
||||
|
||||
@ -110,50 +114,49 @@ namespace osu.Game.Skinning
|
||||
var skinnableTargetWrapper = new DefaultSkinComponentsContainer(container =>
|
||||
{
|
||||
var health = container.OfType<ArgonHealthDisplay>().FirstOrDefault();
|
||||
var score = container.OfType<DefaultScoreCounter>().FirstOrDefault();
|
||||
var accuracy = container.OfType<DefaultAccuracyCounter>().FirstOrDefault();
|
||||
var combo = container.OfType<DefaultComboCounter>().FirstOrDefault();
|
||||
var ppCounter = container.OfType<PerformancePointsCounter>().FirstOrDefault();
|
||||
var healthLine = container.OfType<BoxElement>().FirstOrDefault();
|
||||
var wedgePieces = container.OfType<ArgonWedgePiece>().ToArray();
|
||||
var score = container.OfType<ArgonScoreCounter>().FirstOrDefault();
|
||||
var accuracy = container.OfType<ArgonAccuracyCounter>().FirstOrDefault();
|
||||
var combo = container.OfType<ArgonComboCounter>().FirstOrDefault();
|
||||
var songProgress = container.OfType<ArgonSongProgress>().FirstOrDefault();
|
||||
var keyCounter = container.OfType<ArgonKeyCounterDisplay>().FirstOrDefault();
|
||||
|
||||
if (score != null)
|
||||
if (health != null)
|
||||
{
|
||||
score.Anchor = Anchor.TopCentre;
|
||||
score.Origin = Anchor.TopCentre;
|
||||
|
||||
// elements default to beneath the health bar
|
||||
const float vertical_offset = 30;
|
||||
const float components_x_offset = 50;
|
||||
|
||||
const float horizontal_padding = 20;
|
||||
health.Anchor = Anchor.TopLeft;
|
||||
health.Origin = Anchor.TopLeft;
|
||||
health.UseRelativeSize.Value = false;
|
||||
health.Width = 300;
|
||||
health.BarHeight.Value = 30f;
|
||||
health.Position = new Vector2(components_x_offset, 20f);
|
||||
|
||||
score.Position = new Vector2(0, vertical_offset);
|
||||
|
||||
if (health != null)
|
||||
if (healthLine != null)
|
||||
{
|
||||
health.Origin = Anchor.TopCentre;
|
||||
health.Anchor = Anchor.TopCentre;
|
||||
health.Y = 5;
|
||||
healthLine.Anchor = Anchor.TopLeft;
|
||||
healthLine.Origin = Anchor.CentreLeft;
|
||||
healthLine.Y = health.Y + ArgonHealthDisplay.MAIN_PATH_RADIUS;
|
||||
healthLine.Size = new Vector2(45, 3);
|
||||
}
|
||||
|
||||
if (ppCounter != null)
|
||||
foreach (var wedgePiece in wedgePieces)
|
||||
wedgePiece.Position += new Vector2(-50, 15);
|
||||
|
||||
if (score != null)
|
||||
{
|
||||
ppCounter.Y = score.Position.Y + ppCounter.ScreenSpaceDeltaToParentSpace(score.ScreenSpaceDrawQuad.Size).Y - 4;
|
||||
ppCounter.Origin = Anchor.TopCentre;
|
||||
ppCounter.Anchor = Anchor.TopCentre;
|
||||
score.Origin = Anchor.TopRight;
|
||||
score.Position = new Vector2(components_x_offset + 200, wedgePieces.Last().Y + 30);
|
||||
}
|
||||
|
||||
if (accuracy != null)
|
||||
{
|
||||
accuracy.Position = new Vector2(-accuracy.ScreenSpaceDeltaToParentSpace(score.ScreenSpaceDrawQuad.Size).X / 2 - horizontal_padding, vertical_offset + 5);
|
||||
// +4 to vertically align the accuracy counter with the score counter.
|
||||
accuracy.Position = new Vector2(-20, 20);
|
||||
accuracy.Anchor = Anchor.TopRight;
|
||||
accuracy.Origin = Anchor.TopRight;
|
||||
accuracy.Anchor = Anchor.TopCentre;
|
||||
|
||||
if (combo != null)
|
||||
{
|
||||
combo.Position = new Vector2(accuracy.ScreenSpaceDeltaToParentSpace(score.ScreenSpaceDrawQuad.Size).X / 2 + horizontal_padding, vertical_offset + 5);
|
||||
combo.Anchor = Anchor.TopCentre;
|
||||
}
|
||||
}
|
||||
|
||||
var hitError = container.OfType<HitErrorMeter>().FirstOrDefault();
|
||||
@ -177,34 +180,58 @@ namespace osu.Game.Skinning
|
||||
if (songProgress != null)
|
||||
{
|
||||
const float padding = 10;
|
||||
// Hard to find this at runtime, so taken from the most expanded state during replay.
|
||||
const float song_progress_offset_height = 36 + padding;
|
||||
|
||||
songProgress.Position = new Vector2(0, -padding);
|
||||
songProgress.Scale = new Vector2(0.9f, 1);
|
||||
|
||||
if (keyCounter != null && hitError != null)
|
||||
{
|
||||
// Hard to find this at runtime, so taken from the most expanded state during replay.
|
||||
const float song_progress_offset_height = 36 + padding;
|
||||
|
||||
keyCounter.Anchor = Anchor.BottomRight;
|
||||
keyCounter.Origin = Anchor.BottomRight;
|
||||
keyCounter.Position = new Vector2(-(hitError.Width + padding), -(padding * 2 + song_progress_offset_height));
|
||||
}
|
||||
|
||||
if (combo != null && hitError != null)
|
||||
{
|
||||
combo.Anchor = Anchor.BottomLeft;
|
||||
combo.Origin = Anchor.BottomLeft;
|
||||
combo.Position = new Vector2((hitError.Width + padding), -(padding * 2 + song_progress_offset_height));
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new DefaultComboCounter(),
|
||||
new DefaultScoreCounter(),
|
||||
new DefaultAccuracyCounter(),
|
||||
new ArgonWedgePiece
|
||||
{
|
||||
Size = new Vector2(380, 72),
|
||||
},
|
||||
new ArgonWedgePiece
|
||||
{
|
||||
Size = new Vector2(380, 72),
|
||||
Position = new Vector2(4, 5)
|
||||
},
|
||||
new ArgonScoreCounter
|
||||
{
|
||||
ShowLabel = { Value = false },
|
||||
},
|
||||
new ArgonHealthDisplay(),
|
||||
new BoxElement
|
||||
{
|
||||
CornerRadius = { Value = 0.5f }
|
||||
},
|
||||
new ArgonAccuracyCounter(),
|
||||
new ArgonComboCounter
|
||||
{
|
||||
Scale = new Vector2(1.3f)
|
||||
},
|
||||
new BarHitErrorMeter(),
|
||||
new BarHitErrorMeter(),
|
||||
new ArgonSongProgress(),
|
||||
new ArgonKeyCounterDisplay(),
|
||||
new BarHitErrorMeter(),
|
||||
new BarHitErrorMeter(),
|
||||
new PerformancePointsCounter()
|
||||
}
|
||||
};
|
||||
|
||||
|
53
osu.Game/Skinning/Components/BoxElement.cs
Normal file
53
osu.Game/Skinning/Components/BoxElement.cs
Normal file
@ -0,0 +1,53 @@
|
||||
// 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.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Localisation.SkinComponents;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Skinning.Components
|
||||
{
|
||||
public partial class BoxElement : CompositeDrawable, ISerialisableDrawable
|
||||
{
|
||||
public bool UsesFixedAnchor { get; set; }
|
||||
|
||||
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.CornerRadius), nameof(SkinnableComponentStrings.CornerRadiusDescription),
|
||||
SettingControlType = typeof(SettingsPercentageSlider<float>))]
|
||||
public new BindableFloat CornerRadius { get; } = new BindableFloat(0.25f)
|
||||
{
|
||||
MinValue = 0,
|
||||
MaxValue = 0.5f,
|
||||
Precision = 0.01f
|
||||
};
|
||||
|
||||
public BoxElement()
|
||||
{
|
||||
Size = new Vector2(400, 80);
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = Color4.White,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
};
|
||||
|
||||
Masking = true;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
base.CornerRadius = CornerRadius.Value * Math.Min(DrawWidth, DrawHeight);
|
||||
}
|
||||
}
|
||||
}
|
@ -31,8 +31,7 @@ namespace osu.Game.Skinning
|
||||
: base(
|
||||
skin,
|
||||
resources,
|
||||
// In the case of the actual default legacy skin (ie. the fallback one, which a user hasn't applied any modifications to) we want to use the game provided resources.
|
||||
skin.Protected ? new NamespacedResourceStore<byte[]>(resources.Resources, "Skins/Legacy") : null
|
||||
new NamespacedResourceStore<byte[]>(resources.Resources, "Skins/Legacy")
|
||||
)
|
||||
{
|
||||
Configuration.CustomColours["SliderBall"] = new Color4(2, 170, 255, 255);
|
||||
|
@ -73,7 +73,7 @@ namespace osu.Game.Skinning
|
||||
// needs to be removed else it will cause incorrect skin behaviours. This is due to the config lookup having no context of which skin
|
||||
// it should be returning the version for.
|
||||
|
||||
Skin.LogLookupDebug(this, lookup, Skin.LookupDebugType.Miss);
|
||||
LogLookupDebug(this, lookup, LookupDebugType.Miss);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,6 @@ using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.IO.Stores;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps.Formats;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
@ -51,10 +50,10 @@ namespace osu.Game.Skinning
|
||||
/// </summary>
|
||||
/// <param name="skin">The model for this skin.</param>
|
||||
/// <param name="resources">Access to raw game resources.</param>
|
||||
/// <param name="storage">An optional store which will be used for looking up skin resources. If null, one will be created from realm <see cref="IHasRealmFiles"/> pattern.</param>
|
||||
/// <param name="fallbackStore">An optional fallback store which will be used for file lookups that are not serviced by realm user storage.</param>
|
||||
/// <param name="configurationFilename">The user-facing filename of the configuration file to be parsed. Can accept an .osu or skin.ini file.</param>
|
||||
protected LegacySkin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore<byte[]>? storage, string configurationFilename = @"skin.ini")
|
||||
: base(skin, resources, storage, configurationFilename)
|
||||
protected LegacySkin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore<byte[]>? fallbackStore, string configurationFilename = @"skin.ini")
|
||||
: base(skin, resources, fallbackStore, configurationFilename)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -19,11 +19,16 @@ namespace osu.Game.Skinning
|
||||
public override bool HandleNonPositionalInput => false;
|
||||
public override bool HandlePositionalInput => false;
|
||||
|
||||
public LegacySongProgress()
|
||||
{
|
||||
// User shouldn't be able to adjust width/height of this as `CircularProgress` doesn't
|
||||
// handle stretched cases well.
|
||||
AutoSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Size = new Vector2(33);
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
@ -39,7 +44,7 @@ namespace osu.Game.Skinning
|
||||
},
|
||||
new CircularContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Size = new Vector2(33),
|
||||
Masking = true,
|
||||
BorderColour = Colour4.White,
|
||||
BorderThickness = 2,
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Configuration;
|
||||
@ -18,6 +19,10 @@ namespace osu.Game.Skinning
|
||||
// todo: can probably make this better via deserialisation directly using a common interface.
|
||||
component.Position = drawableInfo.Position;
|
||||
component.Rotation = drawableInfo.Rotation;
|
||||
if (drawableInfo.Width is float width && width != 0 && (component as CompositeDrawable)?.AutoSizeAxes.HasFlagFast(Axes.X) != true)
|
||||
component.Width = width;
|
||||
if (drawableInfo.Height is float height && height != 0 && (component as CompositeDrawable)?.AutoSizeAxes.HasFlagFast(Axes.Y) != true)
|
||||
component.Height = height;
|
||||
component.Scale = drawableInfo.Scale;
|
||||
component.Anchor = drawableInfo.Anchor;
|
||||
component.Origin = drawableInfo.Origin;
|
||||
|
@ -6,6 +6,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Logging;
|
||||
@ -35,6 +36,10 @@ namespace osu.Game.Skinning
|
||||
|
||||
public Vector2 Scale { get; set; }
|
||||
|
||||
public float? Width { get; set; }
|
||||
|
||||
public float? Height { get; set; }
|
||||
|
||||
public Anchor Anchor { get; set; }
|
||||
|
||||
public Anchor Origin { get; set; }
|
||||
@ -62,6 +67,13 @@ namespace osu.Game.Skinning
|
||||
Position = component.Position;
|
||||
Rotation = component.Rotation;
|
||||
Scale = component.Scale;
|
||||
|
||||
if ((component as CompositeDrawable)?.AutoSizeAxes.HasFlagFast(Axes.X) != true)
|
||||
Width = component.Width;
|
||||
|
||||
if ((component as CompositeDrawable)?.AutoSizeAxes.HasFlagFast(Axes.Y) != true)
|
||||
Height = component.Height;
|
||||
|
||||
Anchor = component.Anchor;
|
||||
Origin = component.Origin;
|
||||
|
||||
|
@ -55,7 +55,7 @@ namespace osu.Game.Skinning
|
||||
where TLookup : notnull
|
||||
where TValue : notnull;
|
||||
|
||||
private readonly RealmBackedResourceStore<SkinInfo>? realmBackedStorage;
|
||||
private readonly ResourceStore<byte[]> store = new ResourceStore<byte[]>();
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
@ -64,9 +64,9 @@ namespace osu.Game.Skinning
|
||||
/// </summary>
|
||||
/// <param name="skin">The skin's metadata. Usually a live realm object.</param>
|
||||
/// <param name="resources">Access to game-wide resources.</param>
|
||||
/// <param name="storage">An optional store which will *replace* all file lookups that are usually sourced from <paramref name="skin"/>.</param>
|
||||
/// <param name="fallbackStore">An optional fallback store which will be used for file lookups that are not serviced by realm user storage.</param>
|
||||
/// <param name="configurationFilename">An optional filename to read the skin configuration from. If not provided, the configuration will be retrieved from the storage using "skin.ini".</param>
|
||||
protected Skin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore<byte[]>? storage = null, string configurationFilename = @"skin.ini")
|
||||
protected Skin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore<byte[]>? fallbackStore = null, string configurationFilename = @"skin.ini")
|
||||
{
|
||||
Name = skin.Name;
|
||||
|
||||
@ -74,9 +74,9 @@ namespace osu.Game.Skinning
|
||||
{
|
||||
SkinInfo = skin.ToLive(resources.RealmAccess);
|
||||
|
||||
storage ??= realmBackedStorage = new RealmBackedResourceStore<SkinInfo>(SkinInfo, resources.Files, resources.RealmAccess);
|
||||
store.AddStore(new RealmBackedResourceStore<SkinInfo>(SkinInfo, resources.Files, resources.RealmAccess));
|
||||
|
||||
var samples = resources.AudioManager?.GetSampleStore(storage);
|
||||
var samples = resources.AudioManager?.GetSampleStore(store);
|
||||
|
||||
if (samples != null)
|
||||
{
|
||||
@ -88,7 +88,7 @@ namespace osu.Game.Skinning
|
||||
}
|
||||
|
||||
Samples = samples;
|
||||
Textures = new TextureStore(resources.Renderer, CreateTextureLoaderStore(resources, storage));
|
||||
Textures = new TextureStore(resources.Renderer, CreateTextureLoaderStore(resources, store));
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -96,7 +96,10 @@ namespace osu.Game.Skinning
|
||||
SkinInfo = skin.ToLiveUnmanaged();
|
||||
}
|
||||
|
||||
var configurationStream = storage?.GetStream(configurationFilename);
|
||||
if (fallbackStore != null)
|
||||
store.AddStore(fallbackStore);
|
||||
|
||||
var configurationStream = store.GetStream(configurationFilename);
|
||||
|
||||
if (configurationStream != null)
|
||||
{
|
||||
@ -119,7 +122,7 @@ namespace osu.Game.Skinning
|
||||
{
|
||||
string filename = $"{skinnableTarget}.json";
|
||||
|
||||
byte[]? bytes = storage?.Get(filename);
|
||||
byte[]? bytes = store?.Get(filename);
|
||||
|
||||
if (bytes == null)
|
||||
continue;
|
||||
@ -252,7 +255,7 @@ namespace osu.Game.Skinning
|
||||
Textures?.Dispose();
|
||||
Samples?.Dispose();
|
||||
|
||||
realmBackedStorage?.Dispose();
|
||||
store.Dispose();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
@ -201,8 +201,8 @@ namespace osu.Game.Tests.Visual
|
||||
{
|
||||
private readonly bool extrapolateAnimations;
|
||||
|
||||
public TestLegacySkin(SkinInfo skin, IResourceStore<byte[]> storage, IStorageResourceProvider resources, bool extrapolateAnimations)
|
||||
: base(skin, resources, storage)
|
||||
public TestLegacySkin(SkinInfo skin, IResourceStore<byte[]> fallbackStore, IStorageResourceProvider resources, bool extrapolateAnimations)
|
||||
: base(skin, resources, fallbackStore)
|
||||
{
|
||||
this.extrapolateAnimations = extrapolateAnimations;
|
||||
}
|
||||
|
@ -1,38 +1,31 @@
|
||||
// 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.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Users.Drawables
|
||||
{
|
||||
public partial class ClickableAvatar : OsuClickableContainer
|
||||
public partial class ClickableAvatar : OsuClickableContainer, IHasCustomTooltip<APIUser?>
|
||||
{
|
||||
public override LocalisableString TooltipText
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!Enabled.Value)
|
||||
return string.Empty;
|
||||
public ITooltip<APIUser?> GetCustomTooltip() => showCardOnHover ? new UserCardTooltip() : new NoCardTooltip();
|
||||
|
||||
return ShowUsernameTooltip ? (user?.Username ?? string.Empty) : ContextMenuStrings.ViewProfile;
|
||||
}
|
||||
set => throw new NotSupportedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// By default, the tooltip will show "view profile" as avatars are usually displayed next to a username.
|
||||
/// Setting this to <c>true</c> exposes the username via tooltip for special cases where this is not true.
|
||||
/// </summary>
|
||||
public bool ShowUsernameTooltip { get; set; }
|
||||
public APIUser? TooltipContent { get; }
|
||||
|
||||
private readonly APIUser? user;
|
||||
|
||||
private readonly bool showCardOnHover;
|
||||
|
||||
[Resolved]
|
||||
private OsuGame? game { get; set; }
|
||||
|
||||
@ -40,12 +33,15 @@ namespace osu.Game.Users.Drawables
|
||||
/// A clickable avatar for the specified user, with UI sounds included.
|
||||
/// </summary>
|
||||
/// <param name="user">The user. A null value will get a placeholder avatar.</param>
|
||||
public ClickableAvatar(APIUser? user = null)
|
||||
/// <param name="showCardOnHover">If set to true, the <see cref="UserGridPanel"/> will be shown for the tooltip</param>
|
||||
public ClickableAvatar(APIUser? user = null, bool showCardOnHover = false)
|
||||
{
|
||||
this.user = user;
|
||||
|
||||
if (user?.Id != APIUser.SYSTEM_USER_ID)
|
||||
Action = openProfile;
|
||||
|
||||
this.showCardOnHover = showCardOnHover;
|
||||
|
||||
TooltipContent = this.user = user ?? new GuestUser();
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@ -67,5 +63,65 @@ namespace osu.Game.Users.Drawables
|
||||
|
||||
return base.OnClick(e);
|
||||
}
|
||||
|
||||
public partial class UserCardTooltip : VisibilityContainer, ITooltip<APIUser?>
|
||||
{
|
||||
public UserCardTooltip()
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
protected override void PopIn() => this.FadeIn(150, Easing.OutQuint);
|
||||
protected override void PopOut() => this.Delay(150).FadeOut(500, Easing.OutQuint);
|
||||
|
||||
public void Move(Vector2 pos) => Position = pos;
|
||||
|
||||
private APIUser? user;
|
||||
|
||||
public void SetContent(APIUser? content)
|
||||
{
|
||||
if (content == user && Children.Any())
|
||||
return;
|
||||
|
||||
user = content;
|
||||
|
||||
if (user != null)
|
||||
{
|
||||
LoadComponentAsync(new UserGridPanel(user)
|
||||
{
|
||||
Width = 300,
|
||||
}, panel => Child = panel);
|
||||
}
|
||||
else
|
||||
{
|
||||
var tooltip = new OsuTooltipContainer.OsuTooltip();
|
||||
tooltip.SetContent(ContextMenuStrings.ViewProfile);
|
||||
tooltip.Show();
|
||||
|
||||
Child = tooltip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public partial class NoCardTooltip : VisibilityContainer, ITooltip<APIUser?>
|
||||
{
|
||||
private readonly OsuTooltipContainer.OsuTooltip tooltip;
|
||||
|
||||
public NoCardTooltip()
|
||||
{
|
||||
tooltip = new OsuTooltipContainer.OsuTooltip();
|
||||
tooltip.SetContent(ContextMenuStrings.ViewProfile);
|
||||
Child = tooltip;
|
||||
}
|
||||
|
||||
protected override void PopIn() => tooltip.Show();
|
||||
protected override void PopOut() => tooltip.Hide();
|
||||
|
||||
public void Move(Vector2 pos) => Position = pos;
|
||||
|
||||
public void SetContent(APIUser? content)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -46,21 +46,24 @@ namespace osu.Game.Users.Drawables
|
||||
protected override double LoadDelay => 200;
|
||||
|
||||
private readonly bool isInteractive;
|
||||
private readonly bool showUsernameTooltip;
|
||||
private readonly bool showGuestOnNull;
|
||||
private readonly bool showUserPanelOnHover;
|
||||
|
||||
/// <summary>
|
||||
/// Construct a new UpdateableAvatar.
|
||||
/// </summary>
|
||||
/// <param name="user">The initial user to display.</param>
|
||||
/// <param name="isInteractive">If set to true, hover/click sounds will play and clicking the avatar will open the user's profile.</param>
|
||||
/// <param name="showUsernameTooltip">Whether to show the username rather than "view profile" on the tooltip. (note: this only applies if <paramref name="isInteractive"/> is also true)</param>
|
||||
/// <param name="showUserPanelOnHover">
|
||||
/// If set to true, the user status panel will be displayed in the tooltip.
|
||||
/// Only has an effect if <see cref="isInteractive"/> is true.
|
||||
/// </param>
|
||||
/// <param name="showGuestOnNull">Whether to show a default guest representation on null user (as opposed to nothing).</param>
|
||||
public UpdateableAvatar(APIUser? user = null, bool isInteractive = true, bool showUsernameTooltip = false, bool showGuestOnNull = true)
|
||||
public UpdateableAvatar(APIUser? user = null, bool isInteractive = true, bool showUserPanelOnHover = false, bool showGuestOnNull = true)
|
||||
{
|
||||
this.isInteractive = isInteractive;
|
||||
this.showUsernameTooltip = showUsernameTooltip;
|
||||
this.showGuestOnNull = showGuestOnNull;
|
||||
this.showUserPanelOnHover = showUserPanelOnHover;
|
||||
|
||||
User = user;
|
||||
}
|
||||
@ -72,19 +75,16 @@ namespace osu.Game.Users.Drawables
|
||||
|
||||
if (isInteractive)
|
||||
{
|
||||
return new ClickableAvatar(user)
|
||||
return new ClickableAvatar(user, showUserPanelOnHover)
|
||||
{
|
||||
ShowUsernameTooltip = showUsernameTooltip,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
};
|
||||
}
|
||||
else
|
||||
|
||||
return new DrawableAvatar(user)
|
||||
{
|
||||
return new DrawableAvatar(user)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
};
|
||||
}
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,10 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Users
|
||||
{
|
||||
/// <summary>
|
||||
/// A user "card", commonly used in a grid layout or in popovers.
|
||||
/// Comes with a preset height, but width must be specified.
|
||||
/// </summary>
|
||||
public partial class UserGridPanel : ExtendedUserPanel
|
||||
{
|
||||
private const int margin = 10;
|
||||
|
@ -121,7 +121,7 @@ namespace osu.Game.Utils
|
||||
if (!CheckCompatibleSet(mods, out invalidMods))
|
||||
return false;
|
||||
|
||||
return checkValid(mods, m => m.Type != ModType.System && m.HasImplementation, out invalidMods);
|
||||
return checkValid(mods, m => m.HasImplementation, out invalidMods);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -36,8 +36,8 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Realm" Version="11.5.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2023.1030.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.1109.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2023.1111.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.1114.0" />
|
||||
<PackageReference Include="Sentry" Version="3.40.0" />
|
||||
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->
|
||||
<PackageReference Include="SharpCompress" Version="0.33.0" />
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user