1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-21 01:39:54 +08:00

Compare commits

..

229 Commits

133 changed files with 2216 additions and 1106 deletions
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
timeout-minutes: 5
steps:
- name: Annotate CI run with test results
uses: dorny/test-reporter@v1.4.2
uses: dorny/test-reporter@v1.6.0
with:
artifact: osu-test-results-${{matrix.os.prettyname}}-${{matrix.threadingMode}}
name: Test Results (${{matrix.os.prettyname}}, ${{matrix.threadingMode}})
+5 -2
View File
@@ -51,11 +51,14 @@
<Reference Include="Java.Interop" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.1127.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.1126.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.1207.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.1208.0" />
</ItemGroup>
<ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
<PackageReference Include="Realm" Version="10.18.0" />
</ItemGroup>
<ItemGroup>
<LinkDescription Include="$(MSBuildThisFileDirectory)\osu.Android\Linker.xml"/>
</ItemGroup>
</Project>
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" ?>
<linker>
<assembly fullname="mscorlib">
<!-- see https://github.com/ppy/osu/issues/21516 -->
<type fullname="System.Globalization.*Calendar"/>
</assembly>
</linker>
@@ -4,8 +4,10 @@
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Cursor;
using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
@@ -55,6 +57,21 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods
}
});
[Test]
public void TestGameCursorHidden()
{
CreateModTest(new ModTestData
{
Mod = new CatchModRelax(),
Autoplay = false,
PassCondition = () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<DrawableCatchRuleset>().Single());
return this.ChildrenOfType<MenuCursorContainer>().Single().State.Value == Visibility.Hidden;
}
});
}
private bool passCondition()
{
var playfield = this.ChildrenOfType<CatchPlayfield>().Single();
@@ -0,0 +1,15 @@
// 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.Graphics;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Catch.UI
{
public partial class CatchCursorContainer : GameplayCursorContainer
{
// Just hide the cursor.
// The main goal here is to show that we have a cursor so the game never shows the global one.
protected override Drawable CreateCursor() => Empty();
}
}
@@ -10,6 +10,7 @@ using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
@@ -49,6 +50,8 @@ namespace osu.Game.Rulesets.Catch.UI
this.difficulty = difficulty;
}
protected override GameplayCursorContainer CreateCursor() => new CatchCursorContainer();
[BackgroundDependencyLoader]
private void load()
{
@@ -90,6 +90,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
public override bool CursorInPlacementArea => false;
public TestHitObjectComposer(Playfield playfield)
: base(new ManiaRuleset())
{
Playfield = playfield;
}
@@ -0,0 +1,91 @@
// 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.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Osu.Utils;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
{
[TestFixture]
public class OsuHitObjectGenerationUtilsTest
{
private static Slider createTestSlider()
{
var slider = new Slider
{
Position = new Vector2(128, 128),
Path = new SliderPath
{
ControlPoints =
{
new PathControlPoint(new Vector2(), PathType.Linear),
new PathControlPoint(new Vector2(-64, -128), PathType.Linear), // absolute position: (64, 0)
new PathControlPoint(new Vector2(-128, 0), PathType.Linear) // absolute position: (0, 128)
}
},
RepeatCount = 1
};
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
return slider;
}
[Test]
public void TestReflectSliderHorizontallyAlongPlayfield()
{
var slider = createTestSlider();
OsuHitObjectGenerationUtils.ReflectHorizontallyAlongPlayfield(slider);
Assert.That(slider.Position, Is.EqualTo(new Vector2(OsuPlayfield.BASE_SIZE.X - 128, 128)));
Assert.That(slider.NestedHitObjects.OfType<SliderRepeat>().Single().Position, Is.EqualTo(new Vector2(OsuPlayfield.BASE_SIZE.X - 0, 128)));
Assert.That(slider.Path.ControlPoints.Select(point => point.Position), Is.EquivalentTo(new[]
{
new Vector2(),
new Vector2(64, -128),
new Vector2(128, 0)
}));
}
[Test]
public void TestReflectSliderVerticallyAlongPlayfield()
{
var slider = createTestSlider();
OsuHitObjectGenerationUtils.ReflectVerticallyAlongPlayfield(slider);
Assert.That(slider.Position, Is.EqualTo(new Vector2(128, OsuPlayfield.BASE_SIZE.Y - 128)));
Assert.That(slider.NestedHitObjects.OfType<SliderRepeat>().Single().Position, Is.EqualTo(new Vector2(0, OsuPlayfield.BASE_SIZE.Y - 128)));
Assert.That(slider.Path.ControlPoints.Select(point => point.Position), Is.EquivalentTo(new[]
{
new Vector2(),
new Vector2(-64, 128),
new Vector2(-128, 0)
}));
}
[Test]
public void TestFlipSliderInPlaceHorizontally()
{
var slider = createTestSlider();
OsuHitObjectGenerationUtils.FlipSliderInPlaceHorizontally(slider);
Assert.That(slider.Position, Is.EqualTo(new Vector2(128, 128)));
Assert.That(slider.NestedHitObjects.OfType<SliderRepeat>().Single().Position, Is.EqualTo(new Vector2(256, 128)));
Assert.That(slider.Path.ControlPoints.Select(point => point.Position), Is.EquivalentTo(new[]
{
new Vector2(),
new Vector2(64, -128),
new Vector2(128, 0)
}));
}
}
}
@@ -4,6 +4,8 @@
#nullable disable
using System;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit;
@@ -22,6 +24,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
private bool isPlacingEnd;
[Resolved(CanBeNull = true)]
[CanBeNull]
private IBeatSnapProvider beatSnapProvider { get; set; }
public SpinnerPlacementBlueprint()
: base(new Spinner { Position = OsuPlayfield.BASE_SIZE / 2 })
{
@@ -33,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
base.Update();
if (isPlacingEnd)
HitObject.EndTime = Math.Max(HitObject.StartTime, EditorClock.CurrentTime);
updateEndTimeFromCurrent();
piece.UpdateFrom(HitObject);
}
@@ -45,7 +51,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
if (e.Button != MouseButton.Right)
return false;
HitObject.EndTime = EditorClock.CurrentTime;
updateEndTimeFromCurrent();
EndPlacement(true);
}
else
@@ -61,5 +67,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
return true;
}
private void updateEndTimeFromCurrent()
{
HitObject.EndTime = beatSnapProvider == null
? Math.Max(HitObject.StartTime, EditorClock.CurrentTime)
: Math.Max(HitObject.StartTime + beatSnapProvider.GetBeatLengthAtTime(HitObject.StartTime), beatSnapProvider.SnapTime(EditorClock.CurrentTime));
}
}
}
+1 -1
View File
@@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
var osuObject = (OsuHitObject)hitObject;
OsuHitObjectGenerationUtils.ReflectVertically(osuObject);
OsuHitObjectGenerationUtils.ReflectVerticallyAlongPlayfield(osuObject);
}
}
}
+4 -4
View File
@@ -27,16 +27,16 @@ namespace osu.Game.Rulesets.Osu.Mods
switch (Reflection.Value)
{
case MirrorType.Horizontal:
OsuHitObjectGenerationUtils.ReflectHorizontally(osuObject);
OsuHitObjectGenerationUtils.ReflectHorizontallyAlongPlayfield(osuObject);
break;
case MirrorType.Vertical:
OsuHitObjectGenerationUtils.ReflectVertically(osuObject);
OsuHitObjectGenerationUtils.ReflectVerticallyAlongPlayfield(osuObject);
break;
case MirrorType.Both:
OsuHitObjectGenerationUtils.ReflectHorizontally(osuObject);
OsuHitObjectGenerationUtils.ReflectVertically(osuObject);
OsuHitObjectGenerationUtils.ReflectHorizontallyAlongPlayfield(osuObject);
OsuHitObjectGenerationUtils.ReflectVerticallyAlongPlayfield(osuObject);
break;
}
}
@@ -65,6 +65,11 @@ namespace osu.Game.Rulesets.Osu.Mods
flowDirection = !flowDirection;
}
if (positionInfos[i].HitObject is Slider slider && random.NextDouble() < 0.5)
{
OsuHitObjectGenerationUtils.FlipSliderInPlaceHorizontally(slider);
}
if (i == 0)
{
positionInfos[i].DistanceFromPrevious = (float)(random.NextDouble() * OsuPlayfield.BASE_SIZE.Y / 2);
@@ -17,7 +17,6 @@ using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Beatmaps;
@@ -196,8 +195,8 @@ namespace osu.Game.Rulesets.Osu.Mods
private IEnumerable<double> generateBeats(IBeatmap beatmap, IReadOnlyCollection<OsuHitObject> originalHitObjects)
{
double startTime = originalHitObjects.First().StartTime;
double endTime = originalHitObjects.Last().GetEndTime();
double startTime = beatmap.HitObjects.First().StartTime;
double endTime = beatmap.GetLastObjectTime();
var beats = beatmap.ControlPointInfo.TimingPoints
// Ignore timing points after endTime
@@ -112,44 +112,46 @@ namespace osu.Game.Rulesets.Osu.Utils
/// Reflects the position of the <see cref="OsuHitObject"/> in the playfield horizontally.
/// </summary>
/// <param name="osuObject">The object to reflect.</param>
public static void ReflectHorizontally(OsuHitObject osuObject)
public static void ReflectHorizontallyAlongPlayfield(OsuHitObject osuObject)
{
osuObject.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - osuObject.X, osuObject.Position.Y);
if (!(osuObject is Slider slider))
if (osuObject is not Slider slider)
return;
// No need to update the head and tail circles, since slider handles that when the new slider path is set
slider.NestedHitObjects.OfType<SliderTick>().ForEach(h => h.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - h.Position.X, h.Position.Y));
slider.NestedHitObjects.OfType<SliderRepeat>().ForEach(h => h.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - h.Position.X, h.Position.Y));
void reflectNestedObject(OsuHitObject nested) => nested.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - nested.Position.X, nested.Position.Y);
static void reflectControlPoint(PathControlPoint point) => point.Position = new Vector2(-point.Position.X, point.Position.Y);
var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray();
foreach (var point in controlPoints)
point.Position = new Vector2(-point.Position.X, point.Position.Y);
slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value);
modifySlider(slider, reflectNestedObject, reflectControlPoint);
}
/// <summary>
/// Reflects the position of the <see cref="OsuHitObject"/> in the playfield vertically.
/// </summary>
/// <param name="osuObject">The object to reflect.</param>
public static void ReflectVertically(OsuHitObject osuObject)
public static void ReflectVerticallyAlongPlayfield(OsuHitObject osuObject)
{
osuObject.Position = new Vector2(osuObject.Position.X, OsuPlayfield.BASE_SIZE.Y - osuObject.Y);
if (!(osuObject is Slider slider))
if (osuObject is not Slider slider)
return;
// No need to update the head and tail circles, since slider handles that when the new slider path is set
slider.NestedHitObjects.OfType<SliderTick>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
slider.NestedHitObjects.OfType<SliderRepeat>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
void reflectNestedObject(OsuHitObject nested) => nested.Position = new Vector2(nested.Position.X, OsuPlayfield.BASE_SIZE.Y - nested.Position.Y);
static void reflectControlPoint(PathControlPoint point) => point.Position = new Vector2(point.Position.X, -point.Position.Y);
var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray();
foreach (var point in controlPoints)
point.Position = new Vector2(point.Position.X, -point.Position.Y);
modifySlider(slider, reflectNestedObject, reflectControlPoint);
}
slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value);
/// <summary>
/// Flips the position of the <see cref="Slider"/> around its start position horizontally.
/// </summary>
/// <param name="slider">The slider to be flipped.</param>
public static void FlipSliderInPlaceHorizontally(Slider slider)
{
void flipNestedObject(OsuHitObject nested) => nested.Position = new Vector2(slider.X - (nested.X - slider.X), nested.Y);
static void flipControlPoint(PathControlPoint point) => point.Position = new Vector2(-point.Position.X, point.Position.Y);
modifySlider(slider, flipNestedObject, flipControlPoint);
}
/// <summary>
@@ -160,14 +162,20 @@ namespace osu.Game.Rulesets.Osu.Utils
public static void RotateSlider(Slider slider, float rotation)
{
void rotateNestedObject(OsuHitObject nested) => nested.Position = rotateVector(nested.Position - slider.Position, rotation) + slider.Position;
void rotateControlPoint(PathControlPoint point) => point.Position = rotateVector(point.Position, rotation);
modifySlider(slider, rotateNestedObject, rotateControlPoint);
}
private static void modifySlider(Slider slider, Action<OsuHitObject> modifyNestedObject, Action<PathControlPoint> modifyControlPoint)
{
// No need to update the head and tail circles, since slider handles that when the new slider path is set
slider.NestedHitObjects.OfType<SliderTick>().ForEach(rotateNestedObject);
slider.NestedHitObjects.OfType<SliderRepeat>().ForEach(rotateNestedObject);
slider.NestedHitObjects.OfType<SliderTick>().ForEach(modifyNestedObject);
slider.NestedHitObjects.OfType<SliderRepeat>().ForEach(modifyNestedObject);
var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray();
foreach (var point in controlPoints)
point.Position = rotateVector(point.Position, rotation);
modifyControlPoint(point);
slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value);
}
@@ -0,0 +1,37 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Taiko.Skinning.Legacy;
namespace osu.Game.Rulesets.Taiko.Tests.Skinning
{
public partial class TestSceneTaikoKiaiGlow : TaikoSkinnableTestScene
{
[Test]
public void TestKiaiGlow()
{
AddStep("Create kiai glow", () => SetContents(_ => new LegacyKiaiGlow()));
AddToggleStep("Toggle kiai mode", setUpBeatmap);
}
private void setUpBeatmap(bool withKiai)
{
var controlPointInfo = new ControlPointInfo();
controlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 });
if (withKiai)
controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true });
Beatmap.Value = CreateWorkingBeatmap(new Beatmap
{
ControlPointInfo = controlPointInfo
});
Beatmap.Value.Track.Start();
}
}
}
@@ -0,0 +1,65 @@
// 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.Audio.Track;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
{
internal partial class LegacyKiaiGlow : BeatSyncedContainer
{
private bool isKiaiActive;
private Sprite sprite = null!;
[BackgroundDependencyLoader(true)]
private void load(ISkinSource skin, HealthProcessor? healthProcessor)
{
Child = sprite = new Sprite
{
Texture = skin.GetTexture("taiko-glow"),
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Alpha = 0,
Scale = new Vector2(0.7f),
Colour = new Colour4(255, 228, 0, 255),
};
if (healthProcessor != null)
healthProcessor.NewJudgement += onNewJudgement;
}
protected override void Update()
{
base.Update();
if (isKiaiActive)
sprite.Alpha = (float)Math.Min(1, sprite.Alpha + Math.Abs(Clock.ElapsedFrameTime) / 100f);
else
sprite.Alpha = (float)Math.Max(0, sprite.Alpha - Math.Abs(Clock.ElapsedFrameTime) / 600f);
}
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
{
isKiaiActive = effectPoint.KiaiMode;
}
private void onNewJudgement(JudgementResult result)
{
if (!result.IsHit || !isKiaiActive)
return;
sprite.ScaleTo(0.85f).Then()
.ScaleTo(0.7f, 80, Easing.OutQuad);
}
}
}
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Graphics;
@@ -16,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
{
private Sprite kiai = null!;
private bool kiaiDisplayed;
private bool isKiaiActive;
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
@@ -41,17 +42,19 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
};
}
protected override void Update()
{
base.Update();
if (isKiaiActive)
kiai.Alpha = (float)Math.Min(1, kiai.Alpha + Math.Abs(Clock.ElapsedFrameTime) / 200f);
else
kiai.Alpha = (float)Math.Max(0, kiai.Alpha - Math.Abs(Clock.ElapsedFrameTime) / 200f);
}
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
{
base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes);
if (effectPoint.KiaiMode != kiaiDisplayed)
{
kiaiDisplayed = effectPoint.KiaiMode;
kiai.ClearTransforms();
kiai.FadeTo(kiaiDisplayed ? 1 : 0, 200);
}
isKiaiActive = effectPoint.KiaiMode;
}
}
}
@@ -129,6 +129,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
case TaikoSkinComponents.Mascot:
return new DrawableTaikoMascot();
case TaikoSkinComponents.KiaiGlow:
if (GetTexture("taiko-glow") != null)
return new LegacyKiaiGlow();
return null;
default:
throw new UnsupportedSkinComponentException(lookup);
}
@@ -21,5 +21,6 @@ namespace osu.Game.Rulesets.Taiko
TaikoExplosionKiai,
Scroller,
Mascot,
KiaiGlow
}
}
@@ -112,6 +112,10 @@ namespace osu.Game.Rulesets.Taiko.UI
FillMode = FillMode.Fit,
Children = new[]
{
new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.KiaiGlow), _ => Empty())
{
RelativeSizeAxes = Axes.Both,
},
hitExplosionContainer = new Container<HitExplosion>
{
RelativeSizeAxes = Axes.Both,
@@ -314,6 +314,24 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
[Test]
public void TestGetLastObjectTime()
{
var decoder = new LegacyBeatmapDecoder();
using (var resStream = TestResources.OpenResource("mania-last-object-not-latest.osu"))
using (var stream = new LineBufferedReader(resStream))
{
var beatmap = decoder.Decode(stream);
Assert.That(beatmap.HitObjects.Last().StartTime, Is.EqualTo(2494));
Assert.That(beatmap.HitObjects.Last().GetEndTime(), Is.EqualTo(2494));
Assert.That(beatmap.HitObjects.Max(h => h.GetEndTime()), Is.EqualTo(2582));
Assert.That(beatmap.GetLastObjectTime(), Is.EqualTo(2582));
}
}
[Test]
public void TestDecodeBeatmapComboOffsetsOsu()
{
@@ -16,7 +16,9 @@ using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Replays;
@@ -179,6 +181,40 @@ namespace osu.Game.Tests.Beatmaps.Formats
});
}
[Test]
public void TestSoloScoreData()
{
var ruleset = new OsuRuleset().RulesetInfo;
var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
scoreInfo.Mods = new Mod[]
{
new OsuModDoubleTime { SpeedChange = { Value = 1.1 } }
};
var beatmap = new TestBeatmap(ruleset);
var score = new Score
{
ScoreInfo = scoreInfo,
Replay = new Replay
{
Frames = new List<ReplayFrame>
{
new OsuReplayFrame(2000, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton)
}
}
};
var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap);
Assert.Multiple(() =>
{
Assert.That(decodedAfterEncode.ScoreInfo.Statistics, Is.EqualTo(scoreInfo.Statistics));
Assert.That(decodedAfterEncode.ScoreInfo.MaximumStatistics, Is.EqualTo(scoreInfo.MaximumStatistics));
Assert.That(decodedAfterEncode.ScoreInfo.Mods, Is.EqualTo(scoreInfo.Mods));
});
}
private static Score encodeThenDecode(int beatmapVersion, Score score, TestBeatmap beatmap)
{
var encodeStream = new MemoryStream();
@@ -8,6 +8,7 @@ using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
@@ -137,6 +138,31 @@ namespace osu.Game.Tests.Gameplay
AddAssert("DHO state is correct", () => dho.State.Value == ArmedState.Miss);
}
[Test]
public void TestResultSetBeforeLoadComplete()
{
TestDrawableHitObject dho = null;
HitObjectLifetimeEntry lifetimeEntry = null;
AddStep("Create lifetime entry", () =>
{
var hitObject = new HitObject { StartTime = Time.Current };
lifetimeEntry = new HitObjectLifetimeEntry(hitObject)
{
Result = new JudgementResult(hitObject, hitObject.CreateJudgement())
{
Type = HitResult.Great
}
};
});
AddStep("Create DHO and apply entry", () =>
{
dho = new TestDrawableHitObject();
dho.Apply(lifetimeEntry);
Child = dho;
});
AddAssert("DHO state is correct", () => dho.State.Value, () => Is.EqualTo(ArmedState.Hit));
}
private partial class TestDrawableHitObject : DrawableHitObject
{
public const double INITIAL_LIFETIME_OFFSET = 100;
@@ -0,0 +1,39 @@
osu file format v14
[General]
SampleSet: Normal
StackLeniency: 0.7
Mode: 3
[Difficulty]
HPDrainRate:3
CircleSize:5
OverallDifficulty:8
ApproachRate:8
SliderMultiplier:3.59999990463257
SliderTickRate:2
[TimingPoints]
24,352.941176470588,4,1,1,100,1,0
6376,-50,4,1,1,100,0,0
[HitObjects]
51,192,24,1,0,0:0:0:0:
153,192,200,1,0,0:0:0:0:
358,192,376,1,0,0:0:0:0:
460,192,553,1,0,0:0:0:0:
460,192,729,128,0,1435:0:0:0:0:
358,192,906,128,0,1612:0:0:0:0:
256,192,1082,128,0,1788:0:0:0:0:
153,192,1259,128,0,1965:0:0:0:0:
51,192,1435,128,0,2141:0:0:0:0:
51,192,2318,1,12,0:0:0:0:
153,192,2318,1,4,0:0:0:0:
256,192,2318,1,6,0:0:0:0:
358,192,2318,1,14,0:0:0:0:
460,192,2318,1,0,0:0:0:0:
51,192,2494,128,0,2582:0:0:0:0:
153,192,2494,128,14,2582:0:0:0:0:
256,192,2494,128,6,2582:0:0:0:0:
358,192,2494,128,4,2582:0:0:0:0:
460,192,2494,1,12,0:0:0:0:0:
@@ -7,6 +7,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty;
@@ -355,6 +356,28 @@ namespace osu.Game.Tests.Rulesets.Scoring
}
#pragma warning restore CS0618
[Test]
public void TestAccuracyWhenNearPerfect()
{
const int count_judgements = 1000;
const int count_misses = 1;
double actual = new TestScoreProcessor().ComputeAccuracy(new ScoreInfo
{
Statistics = new Dictionary<HitResult, int>
{
{ HitResult.Great, count_judgements - count_misses },
{ HitResult.Miss, count_misses }
}
});
const double expected = (count_judgements - count_misses) / (double)count_judgements;
Assert.That(actual, Is.Not.EqualTo(0.0));
Assert.That(actual, Is.Not.EqualTo(1.0));
Assert.That(actual, Is.EqualTo(expected).Within(Precision.FLOAT_EPSILON));
}
private class TestRuleset : Ruleset
{
public override IEnumerable<Mod> GetModsFor(ModType type) => throw new NotImplementedException();
+157 -9
View File
@@ -11,7 +11,7 @@ namespace osu.Game.Tests.Utils
public class NamingUtilsTest
{
[Test]
public void TestEmptySet()
public void TestNextBestNameEmptySet()
{
string nextBestName = NamingUtils.GetNextBestName(Enumerable.Empty<string>(), "New Difficulty");
@@ -19,7 +19,7 @@ namespace osu.Game.Tests.Utils
}
[Test]
public void TestNotTaken()
public void TestNextBestNameNotTaken()
{
string[] existingNames =
{
@@ -34,7 +34,7 @@ namespace osu.Game.Tests.Utils
}
[Test]
public void TestNotTakenButClose()
public void TestNextBestNameNotTakenButClose()
{
string[] existingNames =
{
@@ -49,7 +49,7 @@ namespace osu.Game.Tests.Utils
}
[Test]
public void TestAlreadyTaken()
public void TestNextBestNameAlreadyTaken()
{
string[] existingNames =
{
@@ -62,7 +62,7 @@ namespace osu.Game.Tests.Utils
}
[Test]
public void TestAlreadyTakenWithDifferentCase()
public void TestNextBestNameAlreadyTakenWithDifferentCase()
{
string[] existingNames =
{
@@ -75,7 +75,7 @@ namespace osu.Game.Tests.Utils
}
[Test]
public void TestAlreadyTakenWithBrackets()
public void TestNextBestNameAlreadyTakenWithBrackets()
{
string[] existingNames =
{
@@ -88,7 +88,7 @@ namespace osu.Game.Tests.Utils
}
[Test]
public void TestMultipleAlreadyTaken()
public void TestNextBestNameMultipleAlreadyTaken()
{
string[] existingNames =
{
@@ -104,7 +104,7 @@ namespace osu.Game.Tests.Utils
}
[Test]
public void TestEvenMoreAlreadyTaken()
public void TestNextBestNameEvenMoreAlreadyTaken()
{
string[] existingNames = Enumerable.Range(1, 30).Select(i => $"New Difficulty ({i})").Append("New Difficulty").ToArray();
@@ -114,7 +114,7 @@ namespace osu.Game.Tests.Utils
}
[Test]
public void TestMultipleAlreadyTakenWithGaps()
public void TestNextBestNameMultipleAlreadyTakenWithGaps()
{
string[] existingNames =
{
@@ -128,5 +128,153 @@ namespace osu.Game.Tests.Utils
Assert.AreEqual("New Difficulty (2)", nextBestName);
}
[Test]
public void TestNextBestFilenameEmptySet()
{
string nextBestFilename = NamingUtils.GetNextBestFilename(Enumerable.Empty<string>(), "test_file.osr");
Assert.AreEqual("test_file.osr", nextBestFilename);
}
[Test]
public void TestNextBestFilenameNotTaken()
{
string[] existingFiles =
{
"this file exists.zip",
"that file exists.too",
"three.4",
};
string nextBestFilename = NamingUtils.GetNextBestFilename(existingFiles, "test_file.osr");
Assert.AreEqual("test_file.osr", nextBestFilename);
}
[Test]
public void TestNextBestFilenameNotTakenButClose()
{
string[] existingFiles =
{
"replay_file(1).osr",
"replay_file (not a number).zip",
"replay_file (1 <- now THAT is a number right here).lol",
};
string nextBestFilename = NamingUtils.GetNextBestFilename(existingFiles, "replay_file.osr");
Assert.AreEqual("replay_file.osr", nextBestFilename);
}
[Test]
public void TestNextBestFilenameAlreadyTaken()
{
string[] existingFiles =
{
"replay_file.osr",
};
string nextBestFilename = NamingUtils.GetNextBestFilename(existingFiles, "replay_file.osr");
Assert.AreEqual("replay_file (1).osr", nextBestFilename);
}
[Test]
public void TestNextBestFilenameAlreadyTakenDifferentCase()
{
string[] existingFiles =
{
"replay_file.osr",
"RePlAy_FiLe (1).OsR",
"REPLAY_FILE (2).OSR",
};
string nextBestFilename = NamingUtils.GetNextBestFilename(existingFiles, "replay_file.osr");
Assert.AreEqual("replay_file (3).osr", nextBestFilename);
}
[Test]
public void TestNextBestFilenameAlreadyTakenWithBrackets()
{
string[] existingFiles =
{
"replay_file.osr",
"replay_file (copy).osr",
};
string nextBestFilename = NamingUtils.GetNextBestFilename(existingFiles, "replay_file.osr");
Assert.AreEqual("replay_file (1).osr", nextBestFilename);
nextBestFilename = NamingUtils.GetNextBestFilename(existingFiles, "replay_file (copy).osr");
Assert.AreEqual("replay_file (copy) (1).osr", nextBestFilename);
}
[Test]
public void TestNextBestFilenameMultipleAlreadyTaken()
{
string[] existingFiles =
{
"replay_file.osr",
"replay_file (1).osr",
"replay_file (2).osr",
"replay_file (3).osr",
};
string nextBestFilename = NamingUtils.GetNextBestFilename(existingFiles, "replay_file.osr");
Assert.AreEqual("replay_file (4).osr", nextBestFilename);
}
[Test]
public void TestNextBestFilenameMultipleAlreadyTakenWithGaps()
{
string[] existingFiles =
{
"replay_file.osr",
"replay_file (1).osr",
"replay_file (2).osr",
"replay_file (4).osr",
"replay_file (5).osr",
};
string nextBestFilename = NamingUtils.GetNextBestFilename(existingFiles, "replay_file.osr");
Assert.AreEqual("replay_file (3).osr", nextBestFilename);
}
[Test]
public void TestNextBestFilenameNoExtensions()
{
string[] existingFiles =
{
"those",
"are definitely",
"files",
};
string nextBestFilename = NamingUtils.GetNextBestFilename(existingFiles, "surely");
Assert.AreEqual("surely", nextBestFilename);
nextBestFilename = NamingUtils.GetNextBestFilename(existingFiles, "those");
Assert.AreEqual("those (1)", nextBestFilename);
}
[Test]
public void TestNextBestFilenameDifferentExtensions()
{
string[] existingFiles =
{
"replay_file.osr",
"replay_file (1).osr",
"replay_file.txt",
};
string nextBestFilename = NamingUtils.GetNextBestFilename(existingFiles, "replay_file.osr");
Assert.AreEqual("replay_file (2).osr", nextBestFilename);
nextBestFilename = NamingUtils.GetNextBestFilename(existingFiles, "replay_file.txt");
Assert.AreEqual("replay_file (1).txt", nextBestFilename);
}
}
}
@@ -7,12 +7,14 @@ using osu.Framework.Graphics.Shapes;
using osuTK;
using osuTK.Graphics;
using osu.Game.Graphics.Backgrounds;
using osu.Framework.Graphics.Colour;
namespace osu.Game.Tests.Visual.Background
{
public partial class TestSceneTrianglesV2Background : OsuTestScene
{
private readonly TrianglesV2 triangles;
private readonly Box box;
public TestSceneTrianglesV2Background()
{
@@ -23,27 +25,44 @@ namespace osu.Game.Tests.Visual.Background
RelativeSizeAxes = Axes.Both,
Colour = Color4.Gray
},
new Container
new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(500, 100),
Masking = true,
CornerRadius = 40,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 5),
Children = new Drawable[]
{
new Box
new Container
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Red
Size = new Vector2(500, 100),
Masking = true,
CornerRadius = 40,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Red
},
triangles = new TrianglesV2
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both
}
}
},
triangles = new TrianglesV2
new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
ColourTop = Color4.White,
ColourBottom = Color4.Red
Size = new Vector2(500, 100),
Masking = true,
CornerRadius = 40,
Child = box = new Box
{
RelativeSizeAxes = Axes.Both
}
}
}
}
@@ -54,8 +73,16 @@ namespace osu.Game.Tests.Visual.Background
{
base.LoadComplete();
AddSliderStep("Spawn ratio", 0f, 2f, 1f, s => triangles.SpawnRatio = s);
AddSliderStep("Spawn ratio", 0f, 10f, 1f, s =>
{
triangles.SpawnRatio = s;
triangles.Reset(1234);
});
AddSliderStep("Thickness", 0f, 1f, 0.02f, t => triangles.Thickness = t);
AddStep("White colour", () => box.Colour = triangles.Colour = Color4.White);
AddStep("Vertical gradient", () => box.Colour = triangles.Colour = ColourInfo.GradientVertical(Color4.White, Color4.Red));
AddStep("Horizontal gradient", () => box.Colour = triangles.Colour = ColourInfo.GradientHorizontal(Color4.White, Color4.Red));
}
}
}
@@ -10,6 +10,8 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Containers;
using osu.Game.Overlays;
using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit;
@@ -26,6 +28,7 @@ namespace osu.Game.Tests.Visual.Editing
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
private TimingScreen timingScreen;
private EditorBeatmap editorBeatmap;
protected override bool ScrollUsingMouseWheel => false;
@@ -35,8 +38,11 @@ namespace osu.Game.Tests.Visual.Editing
Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value);
Beatmap.Disabled = true;
}
var editorBeatmap = new EditorBeatmap(Beatmap.Value.GetPlayableBeatmap(Ruleset.Value));
private void reloadEditorBeatmap()
{
editorBeatmap = new EditorBeatmap(Beatmap.Value.GetPlayableBeatmap(Ruleset.Value));
Child = new DependencyProvidingContainer
{
@@ -58,7 +64,9 @@ namespace osu.Game.Tests.Visual.Editing
{
AddStep("Stop clock", () => EditorClock.Stop());
AddUntilStep("wait for rows to load", () => Child.ChildrenOfType<EffectRowAttribute>().Any());
AddStep("Reload Editor Beatmap", reloadEditorBeatmap);
AddUntilStep("Wait for rows to load", () => Child.ChildrenOfType<EffectRowAttribute>().Any());
}
[Test]
@@ -95,6 +103,37 @@ namespace osu.Game.Tests.Visual.Editing
AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 69670);
}
[Test]
public void TestScrollControlGroupIntoView()
{
AddStep("Add many control points", () =>
{
editorBeatmap.ControlPointInfo.Clear();
editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint());
for (int i = 0; i < 100; i++)
{
editorBeatmap.ControlPointInfo.Add((i + 1) * 1000, new EffectControlPoint
{
KiaiMode = Convert.ToBoolean(i % 2),
});
}
});
AddStep("Select first effect point", () =>
{
InputManager.MoveMouseTo(Child.ChildrenOfType<EffectRowAttribute>().First());
InputManager.Click(MouseButton.Left);
});
AddStep("Seek to beginning", () => EditorClock.Seek(0));
AddStep("Seek to last point", () => EditorClock.Seek(101 * 1000));
AddUntilStep("Scrolled to end", () => timingScreen.ChildrenOfType<OsuScrollContainer>().First().IsScrolledToEnd());
}
protected override void Dispose(bool isDisposing)
{
Beatmap.Disabled = false;
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
@@ -187,18 +188,22 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestInputDoesntWorkWhenHUDHidden()
{
SongProgressBar getSongProgress() => hudOverlay.ChildrenOfType<SongProgressBar>().Single();
SongProgressBar? getSongProgress() => hudOverlay.ChildrenOfType<SongProgressBar>().SingleOrDefault();
bool seeked = false;
createNew();
AddUntilStep("wait for song progress", () => getSongProgress() != null);
AddStep("bind seek", () =>
{
seeked = false;
var progress = getSongProgress();
Debug.Assert(progress != null);
progress.ShowHandle = true;
progress.OnSeek += _ => seeked = true;
});
@@ -107,7 +107,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for load", () => downloadButton.IsLoaded);
AddAssert("state is available", () => downloadButton.State.Value == DownloadState.NotDownloaded);
checkState(DownloadState.NotDownloaded);
AddStep("click button", () =>
{
@@ -133,7 +133,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for load", () => downloadButton.IsLoaded);
AddAssert("state is not downloaded", () => downloadButton.State.Value == DownloadState.NotDownloaded);
checkState(DownloadState.NotDownloaded);
AddAssert("button is not enabled", () => !downloadButton.ChildrenOfType<DownloadButton>().First().Enabled.Value);
}
@@ -155,7 +155,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for load", () => downloadButton.IsLoaded);
AddUntilStep("state is not downloaded", () => downloadButton.State.Value == DownloadState.NotDownloaded);
checkState(DownloadState.NotDownloaded);
AddAssert("button is not enabled", () => !downloadButton.ChildrenOfType<DownloadButton>().First().Enabled.Value);
}
@@ -174,17 +174,16 @@ namespace osu.Game.Tests.Visual.Gameplay
});
AddUntilStep("wait for load", () => downloadButton.IsLoaded);
AddUntilStep("state is not downloaded", () => downloadButton.State.Value == DownloadState.NotDownloaded);
checkState(DownloadState.NotDownloaded);
AddStep("import score", () => imported = scoreManager.Import(getScoreInfo(true)));
AddUntilStep("state is available", () => downloadButton.State.Value == DownloadState.LocallyAvailable);
checkState(DownloadState.LocallyAvailable);
AddAssert("button is enabled", () => downloadButton.ChildrenOfType<DownloadButton>().First().Enabled.Value);
AddStep("delete score", () => scoreManager.Delete(imported.Value));
AddUntilStep("state is not downloaded", () => downloadButton.State.Value == DownloadState.NotDownloaded);
checkState(DownloadState.NotDownloaded);
AddAssert("button is not enabled", () => !downloadButton.ChildrenOfType<DownloadButton>().First().Enabled.Value);
}
@@ -202,10 +201,13 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for load", () => downloadButton.IsLoaded);
AddAssert("state is unknown", () => downloadButton.State.Value == DownloadState.Unknown);
checkState(DownloadState.Unknown);
AddAssert("button is not enabled", () => !downloadButton.ChildrenOfType<DownloadButton>().First().Enabled.Value);
}
private void checkState(DownloadState expectedState) =>
AddUntilStep($"state is {expectedState}", () => downloadButton.State.Value, () => Is.EqualTo(expectedState));
private ScoreInfo getScoreInfo(bool replayAvailable, bool hasOnlineId = true) => new ScoreInfo
{
OnlineID = hasOnlineId ? online_score_id : 0,
@@ -261,7 +261,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestFinalFramesPurgedBeforeEndingPlay()
{
AddStep("begin playing", () => spectatorClient.BeginPlaying(TestGameplayState.Create(new OsuRuleset()), new Score()));
AddStep("begin playing", () => spectatorClient.BeginPlaying(0, TestGameplayState.Create(new OsuRuleset()), new Score()));
AddStep("send frames and finish play", () =>
{
@@ -147,7 +147,7 @@ namespace osu.Game.Tests.Visual.Gameplay
}
};
spectatorClient.BeginPlaying(TestGameplayState.Create(new OsuRuleset()), recordingScore);
spectatorClient.BeginPlaying(0, TestGameplayState.Create(new OsuRuleset()), recordingScore);
spectatorClient.OnNewFrames += onNewFrames;
});
}
@@ -117,11 +117,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
BeatmapID = 0,
RulesetID = 0,
Mods = user.Mods,
MaximumScoringValues = new ScoringValues
MaximumStatistics = new Dictionary<HitResult, int>
{
BaseScore = 10000,
MaxCombo = 1000,
CountBasicHitObjects = 1000
{ HitResult.Perfect, 100 }
}
};
}
@@ -130,11 +130,11 @@ namespace osu.Game.Tests.Visual.Online
Color4 textColour = isAction && hasBackground ? Color4Extensions.FromHex(newLine.Message.Sender.Colour) : Color4.White;
var linkCompilers = newLine.ContentFlow.Where(d => d is DrawableLinkCompiler).ToList();
var linkCompilers = newLine.DrawableContentFlow.Where(d => d is DrawableLinkCompiler).ToList();
var linkSprites = linkCompilers.SelectMany(comp => ((DrawableLinkCompiler)comp).Parts);
return linkSprites.All(d => d.Colour == linkColour)
&& newLine.ContentFlow.Except(linkSprites.Concat(linkCompilers)).All(d => d.Colour == textColour);
&& newLine.DrawableContentFlow.Except(linkSprites.Concat(linkCompilers)).All(d => d.Colour == textColour);
}
}
}
@@ -11,6 +11,7 @@ using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Online.Chat;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Online
@@ -33,7 +34,7 @@ namespace osu.Game.Tests.Visual.Online
{
AddStep("Set activity", () => api.Activity.Value = new UserActivity.InLobby(null));
AddStep("Run command", () => Add(new NowPlayingCommand()));
AddStep("Run command", () => Add(new NowPlayingCommand(new Channel())));
AddAssert("Check correct response", () => postTarget.LastMessage.Contains("is listening"));
}
@@ -43,7 +44,7 @@ namespace osu.Game.Tests.Visual.Online
{
AddStep("Set activity", () => api.Activity.Value = new UserActivity.Editing(new BeatmapInfo()));
AddStep("Run command", () => Add(new NowPlayingCommand()));
AddStep("Run command", () => Add(new NowPlayingCommand(new Channel())));
AddAssert("Check correct response", () => postTarget.LastMessage.Contains("is editing"));
}
@@ -53,7 +54,7 @@ namespace osu.Game.Tests.Visual.Online
{
AddStep("Set activity", () => api.Activity.Value = new UserActivity.InSoloGame(new BeatmapInfo(), new RulesetInfo()));
AddStep("Run command", () => Add(new NowPlayingCommand()));
AddStep("Run command", () => Add(new NowPlayingCommand(new Channel())));
AddAssert("Check correct response", () => postTarget.LastMessage.Contains("is playing"));
}
@@ -69,7 +70,7 @@ namespace osu.Game.Tests.Visual.Online
BeatmapInfo = { OnlineID = hasOnlineId ? 1234 : -1 }
});
AddStep("Run command", () => Add(new NowPlayingCommand()));
AddStep("Run command", () => Add(new NowPlayingCommand(new Channel())));
if (hasOnlineId)
AddAssert("Check link presence", () => postTarget.LastMessage.Contains("/b/1234"));
@@ -77,6 +78,18 @@ namespace osu.Game.Tests.Visual.Online
AddAssert("Check link not present", () => !postTarget.LastMessage.Contains("https://"));
}
[Test]
public void TestModPresence()
{
AddStep("Set activity", () => api.Activity.Value = new UserActivity.InSoloGame(new BeatmapInfo(), new RulesetInfo()));
AddStep("Add Hidden mod", () => SelectedMods.Value = new[] { Ruleset.Value.CreateInstance().CreateMod<ModHidden>() });
AddStep("Run command", () => Add(new NowPlayingCommand(new Channel())));
AddAssert("Check mod is present", () => postTarget.LastMessage.Contains("+HD"));
}
public partial class PostTarget : Component, IChannelPostTarget
{
public void PostMessage(string text, bool isAction = false, Channel target = null)
@@ -73,6 +73,11 @@ namespace osu.Game.Tests.Visual.Online
messageIdSequence = 0;
channelManager.CurrentChannel.Value = testChannel = new Channel();
reinitialiseDrawableDisplay();
});
private void reinitialiseDrawableDisplay()
{
Children = new[]
{
chatDisplay = new TestStandAloneChatDisplay
@@ -92,13 +97,14 @@ namespace osu.Game.Tests.Visual.Online
Channel = { Value = testChannel },
}
};
});
}
[Test]
public void TestSystemMessageOrdering()
{
var standardMessage = new Message(messageIdSequence++)
{
Timestamp = DateTimeOffset.Now,
Sender = admin,
Content = "I am a wang!"
};
@@ -106,14 +112,45 @@ namespace osu.Game.Tests.Visual.Online
var infoMessage1 = new InfoMessage($"the system is calling {messageIdSequence++}");
var infoMessage2 = new InfoMessage($"the system is calling {messageIdSequence++}");
var standardMessage2 = new Message(messageIdSequence++)
{
Timestamp = DateTimeOffset.Now,
Sender = admin,
Content = "I am a wang!"
};
AddStep("message from admin", () => testChannel.AddNewMessages(standardMessage));
AddStep("message from system", () => testChannel.AddNewMessages(infoMessage1));
AddStep("message from system", () => testChannel.AddNewMessages(infoMessage2));
AddStep("message from admin", () => testChannel.AddNewMessages(standardMessage2));
AddAssert("message order is correct", () => testChannel.Messages.Count == 3
&& testChannel.Messages[0] == standardMessage
&& testChannel.Messages[1] == infoMessage1
&& testChannel.Messages[2] == infoMessage2);
AddAssert("count is correct", () => testChannel.Messages.Count, () => Is.EqualTo(4));
AddAssert("message order is correct", () => testChannel.Messages, () => Is.EqualTo(new[]
{
standardMessage,
infoMessage1,
infoMessage2,
standardMessage2
}));
AddAssert("displayed order is correct", () => chatDisplay.DrawableChannel.ChildrenOfType<ChatLine>().Select(c => c.Message), () => Is.EqualTo(new[]
{
standardMessage,
infoMessage1,
infoMessage2,
standardMessage2
}));
AddStep("reinit drawable channel", reinitialiseDrawableDisplay);
AddAssert("displayed order is still correct", () => chatDisplay.DrawableChannel.ChildrenOfType<ChatLine>().Select(c => c.Message), () => Is.EqualTo(new[]
{
standardMessage,
infoMessage1,
infoMessage2,
standardMessage2
}));
}
[Test]
@@ -62,7 +62,6 @@ namespace osu.Game.Tests.Visual.Settings
section.Children.Where(f => f.IsPresent)
.OfType<ISettingsItem>()
.OfType<IFilterable>()
.Where(f => !(f is IHasFilterableChildren))
.All(f => f.FilterTerms.Any(t => t.ToString().Contains("scaling")))
));
@@ -1,28 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Screens.Select;
using osuTK;
namespace osu.Game.Tests.Visual.SongSelect
{
public partial class TestSceneDifficultyRangeFilterControl : OsuTestScene
{
[Test]
public void TestBasic()
{
AddStep("create control", () =>
{
Child = new DifficultyRangeFilterControl
{
Width = 200,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(3),
};
});
}
}
}
@@ -0,0 +1,129 @@
// 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.Graphics.Containers;
using osu.Framework.Graphics;
using osu.Game.Overlays.Settings;
using NUnit.Framework;
using osuTK;
using osu.Game.Overlays;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Graphics.UserInterface;
using osu.Framework.Allocation;
using osu.Game.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osuTK.Graphics;
using osu.Game.Graphics.Sprites;
namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneButtonsInput : OsuManualInputManagerTestScene
{
private const int width = 500;
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
private readonly SettingsButton settingsButton;
private readonly OsuClickableContainer clickableContainer;
private readonly RoundedButton roundedButton;
private readonly ShearedButton shearedButton;
public TestSceneButtonsInput()
{
Add(new FillFlowContainer
{
AutoSizeAxes = Axes.Y,
Width = 500,
Spacing = new Vector2(0, 5),
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
clickableContainer = new OsuClickableContainer
{
RelativeSizeAxes = Axes.X,
Height = 40,
Enabled = { Value = true },
Masking = true,
CornerRadius = 20,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Red
},
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = "Rounded clickable container"
}
}
},
settingsButton = new SettingsButton
{
Enabled = { Value = true },
Text = "Settings button"
},
roundedButton = new RoundedButton
{
RelativeSizeAxes = Axes.X,
Enabled = { Value = true },
Text = "Rounded button"
},
shearedButton = new ShearedButton(width)
{
Text = "Sheared button",
LighterColour = Colour4.FromHex("#FFFFFF"),
DarkerColour = Colour4.FromHex("#FFCC22"),
TextColour = Colour4.Black,
Height = 40,
Enabled = { Value = true },
Padding = new MarginPadding(0)
}
}
});
}
[Test]
public void TestSettingsButtonInput()
{
AddStep("Move cursor to button", () => InputManager.MoveMouseTo(settingsButton));
AddAssert("Button is hovered", () => settingsButton.IsHovered);
AddStep("Move cursor to padded area", () => InputManager.MoveMouseTo(settingsButton.ScreenSpaceDrawQuad.TopLeft + new Vector2(SettingsPanel.CONTENT_MARGINS / 2f, 10)));
AddAssert("Cursor within a button", () => settingsButton.ScreenSpaceDrawQuad.Contains(InputManager.CurrentState.Mouse.Position));
AddAssert("Button is not hovered", () => !settingsButton.IsHovered);
}
[Test]
public void TestRoundedButtonInput()
{
AddStep("Move cursor to button", () => InputManager.MoveMouseTo(roundedButton));
AddAssert("Button is hovered", () => roundedButton.IsHovered);
AddStep("Move cursor to corner", () => InputManager.MoveMouseTo(roundedButton.ScreenSpaceDrawQuad.TopLeft + Vector2.One));
AddAssert("Cursor within a button", () => roundedButton.ScreenSpaceDrawQuad.Contains(InputManager.CurrentState.Mouse.Position));
AddAssert("Button is not hovered", () => !roundedButton.IsHovered);
}
[Test]
public void TestShearedButtonInput()
{
AddStep("Move cursor to button", () => InputManager.MoveMouseTo(shearedButton));
AddAssert("Button is hovered", () => shearedButton.IsHovered);
AddStep("Move cursor to corner", () => InputManager.MoveMouseTo(shearedButton.ScreenSpaceDrawQuad.TopLeft + Vector2.One));
AddAssert("Cursor within a button", () => shearedButton.ScreenSpaceDrawQuad.Contains(InputManager.CurrentState.Mouse.Position));
AddAssert("Button is not hovered", () => !shearedButton.IsHovered);
}
[Test]
public void TestRoundedClickableContainerInput()
{
AddStep("Move cursor to button", () => InputManager.MoveMouseTo(clickableContainer));
AddAssert("Button is hovered", () => clickableContainer.IsHovered);
AddStep("Move cursor to corner", () => InputManager.MoveMouseTo(clickableContainer.ScreenSpaceDrawQuad.TopLeft + Vector2.One));
AddAssert("Cursor within a button", () => clickableContainer.ScreenSpaceDrawQuad.Contains(InputManager.CurrentState.Mouse.Position));
AddAssert("Button is not hovered", () => !clickableContainer.IsHovered);
}
}
}
@@ -126,6 +126,21 @@ namespace osu.Game.Tests.Visual.UserInterface
checkBindableAtValue("Circle Size", 9);
}
[Test]
public void TestExtendedLimitsRetainedAfterBoundCopyCreation()
{
setExtendedLimits(true);
setSliderValue("Circle Size", 11);
checkSliderAtValue("Circle Size", 11);
checkBindableAtValue("Circle Size", 11);
AddStep("create bound copy", () => _ = modDifficultyAdjust.CircleSize.GetBoundCopy());
checkSliderAtValue("Circle Size", 11);
checkBindableAtValue("Circle Size", 11);
}
[Test]
public void TestResetToDefault()
{
@@ -1,47 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Graphics.UserInterface;
using osuTK;
namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneOsuButton : OsuTestScene
{
[Test]
public void TestToggleEnabled()
{
OsuButton button = null;
AddStep("add button", () => Child = button = new OsuButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(200),
Text = "Button"
});
AddToggleStep("toggle enabled", toggle =>
{
for (int i = 0; i < 6; i++)
button.Action = toggle ? () => { } : null;
});
}
[Test]
public void TestInitiallyDisabled()
{
AddStep("add button", () => Child = new OsuButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(200),
Text = "Button"
});
}
}
}
@@ -0,0 +1,79 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneRangeSlider : OsuTestScene
{
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Red);
private readonly BindableNumber<double> customStart = new BindableNumber<double>
{
MinValue = 0,
MaxValue = 100,
Precision = 0.1f
};
private readonly BindableNumber<double> customEnd = new BindableNumber<double>(100)
{
MinValue = 0,
MaxValue = 100,
Precision = 0.1f
};
private RangeSlider rangeSlider = null!;
[SetUpSteps]
public void SetUpSteps()
{
AddStep("create control", () => Child = rangeSlider = new RangeSlider
{
Width = 200,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(3),
LowerBound = customStart,
UpperBound = customEnd,
TooltipSuffix = "suffix",
NubWidth = Nub.HEIGHT * 2,
DefaultStringLowerBound = "Start",
DefaultStringUpperBound = "End",
MinRange = 10
});
}
[Test]
public void TestAdjustRange()
{
AddAssert("Initial lower bound is correct", () => rangeSlider.LowerBound.Value, () => Is.EqualTo(0).Within(0.1f));
AddAssert("Initial upper bound is correct", () => rangeSlider.UpperBound.Value, () => Is.EqualTo(100).Within(0.1f));
AddStep("Adjust range", () =>
{
customStart.Value = 50;
customEnd.Value = 75;
});
AddAssert("Adjusted lower bound is correct", () => rangeSlider.LowerBound.Value, () => Is.EqualTo(50).Within(0.1f));
AddAssert("Adjusted upper bound is correct", () => rangeSlider.UpperBound.Value, () => Is.EqualTo(75).Within(0.1f));
AddStep("Test nub pushing", () =>
{
customStart.Value = 90;
});
AddAssert("Pushed lower bound is correct", () => rangeSlider.LowerBound.Value, () => Is.EqualTo(90).Within(0.1f));
AddAssert("Pushed upper bound is correct", () => rangeSlider.UpperBound.Value, () => Is.EqualTo(100).Within(0.1f));
}
}
}
+2
View File
@@ -109,6 +109,8 @@ namespace osu.Game.Audio
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
Stop();
Track?.Dispose();
}
}
+6 -1
View File
@@ -81,9 +81,14 @@ namespace osu.Game.Beatmaps
public double GetMostCommonBeatLength()
{
double lastTime;
// The last playable time in the beatmap - the last timing point extends to this time.
// Note: This is more accurate and may present different results because osu-stable didn't have the ability to calculate slider durations in this context.
double lastTime = HitObjects.LastOrDefault()?.GetEndTime() ?? ControlPointInfo.TimingPoints.LastOrDefault()?.Time ?? 0;
if (!HitObjects.Any())
lastTime = ControlPointInfo.TimingPoints.LastOrDefault()?.Time ?? 0;
else
lastTime = this.GetLastObjectTime();
var mostCommon =
// Construct a set of (beatLength, duration) tuples for each individual timing point.
@@ -11,7 +11,7 @@ using osu.Game.Beatmaps.Drawables.Cards.Buttons;
using osu.Game.Graphics;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Screens.Ranking.Expanded.Accuracy;
using osu.Framework.Graphics.UserInterface;
using osuTK;
using osuTK.Graphics;
@@ -30,7 +30,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
private readonly UpdateableOnlineBeatmapSetCover cover;
private readonly Container foreground;
private readonly PlayButton playButton;
private readonly SmoothCircularProgress progress;
private readonly CircularProgress progress;
private readonly Container content;
protected override Container<Drawable> Content => content;
@@ -53,7 +53,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{
RelativeSizeAxes = Axes.Both
},
progress = new SmoothCircularProgress
progress = new CircularProgress
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
+12
View File
@@ -4,6 +4,7 @@
#nullable disable
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Objects;
@@ -102,5 +103,16 @@ namespace osu.Game.Beatmaps
addCombo(nested, ref combo);
}
}
/// <summary>
/// Find the absolute end time of the latest <see cref="HitObject"/> in a beatmap. Will throw if beatmap contains no objects.
/// </summary>
/// <remarks>
/// This correctly accounts for rulesets which have concurrent hitobjects which may have durations, causing the .Last() object
/// to not necessarily have the latest end time.
///
/// It's not super efficient so calls should be kept to a minimum.
/// </remarks>
public static double GetLastObjectTime(this IBeatmap beatmap) => beatmap.HitObjects.Max(h => h.GetEndTime());
}
}
+6 -1
View File
@@ -3,9 +3,11 @@
#nullable disable
using System.Collections.Generic;
using System.IO;
using osu.Framework.Platform;
using osu.Game.Extensions;
using osu.Game.Utils;
using SharpCompress.Archives.Zip;
namespace osu.Game.Database
@@ -37,8 +39,11 @@ namespace osu.Game.Database
/// <param name="item">The item to export.</param>
public void Export(TModel item)
{
string filename = $"{item.GetDisplayString().GetValidFilename()}{FileExtension}";
string itemFilename = item.GetDisplayString().GetValidFilename();
IEnumerable<string> existingExports = exportStorage.GetFiles("", $"{itemFilename}*{FileExtension}");
string filename = NamingUtils.GetNextBestFilename(existingExports, $"{itemFilename}{FileExtension}");
using (var stream = exportStorage.CreateFileSafely(filename))
ExportModelTo(item, stream);
+3 -3
View File
@@ -42,8 +42,8 @@ namespace osu.Game.Database
[Resolved]
private RealmAccess realmAccess { get; set; } = null!;
[Resolved(canBeNull: true)] // canBeNull required while we remain on mono for mobile platforms.
private DesktopGameHost? desktopGameHost { get; set; }
[Resolved]
private GameHost gameHost { get; set; } = null!;
[Resolved]
private INotificationOverlay? notifications { get; set; }
@@ -52,7 +52,7 @@ namespace osu.Game.Database
public bool SupportsImportFromStable => RuntimeInfo.IsDesktop;
public void UpdateStorage(string stablePath) => cachedStorage = new StableStorage(stablePath, desktopGameHost);
public void UpdateStorage(string stablePath) => cachedStorage = new StableStorage(stablePath, gameHost as DesktopGameHost);
public virtual async Task<int> GetImportCount(StableContent content, CancellationToken cancellationToken)
{
+26 -64
View File
@@ -11,9 +11,7 @@ using osu.Framework.Allocation;
using System.Collections.Generic;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Rendering.Vertices;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp;
using osuTK.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -23,28 +21,12 @@ namespace osu.Game.Graphics.Backgrounds
{
private const float triangle_size = 100;
private const float base_velocity = 50;
private const int texture_height = 128;
/// <summary>
/// sqrt(3) / 2
/// </summary>
private const float equilateral_triangle_ratio = 0.866f;
private readonly Bindable<Color4> colourTop = new Bindable<Color4>(Color4.White);
private readonly Bindable<Color4> colourBottom = new Bindable<Color4>(Color4.Black);
public Color4 ColourTop
{
get => colourTop.Value;
set => colourTop.Value = value;
}
public Color4 ColourBottom
{
get => colourBottom.Value;
set => colourBottom.Value = value;
}
public float Thickness { get; set; } = 0.02f; // No need for invalidation since it's happening in Update()
/// <summary>
@@ -70,9 +52,6 @@ namespace osu.Game.Graphics.Backgrounds
private readonly List<TriangleParticle> parts = new List<TriangleParticle>();
[Resolved]
private IRenderer renderer { get; set; } = null!;
private Random? stableRandom;
private IShader shader = null!;
@@ -89,42 +68,19 @@ namespace osu.Game.Graphics.Backgrounds
}
[BackgroundDependencyLoader]
private void load(ShaderManager shaders)
private void load(ShaderManager shaders, IRenderer renderer)
{
shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, "TriangleBorder");
texture = renderer.WhitePixel;
}
protected override void LoadComplete()
{
base.LoadComplete();
colourTop.BindValueChanged(_ => updateTexture());
colourBottom.BindValueChanged(_ => updateTexture(), true);
spawnRatio.BindValueChanged(_ => Reset(), true);
}
private void updateTexture()
{
var image = new Image<Rgba32>(texture_height, 1);
texture = renderer.CreateTexture(1, texture_height, true);
for (int i = 0; i < texture_height; i++)
{
float ratio = (float)i / texture_height;
image[i, 0] = new Rgba32(
colourBottom.Value.R * ratio + colourTop.Value.R * (1f - ratio),
colourBottom.Value.G * ratio + colourTop.Value.G * (1f - ratio),
colourBottom.Value.B * ratio + colourTop.Value.B * (1f - ratio)
);
}
texture.SetData(new TextureUpload(image));
Invalidate(Invalidation.DrawNode);
}
protected override void Update()
{
base.Update();
@@ -280,36 +236,42 @@ namespace osu.Game.Graphics.Backgrounds
shader.GetUniform<float>("thickness").UpdateValue(ref thickness);
shader.GetUniform<float>("texelSize").UpdateValue(ref texelSize);
float texturePartWidth = triangleSize.X / size.X;
float texturePartHeight = triangleSize.Y / size.Y * texture_height;
float relativeHeight = triangleSize.Y / size.Y;
float relativeWidth = triangleSize.X / size.X;
foreach (TriangleParticle particle in parts)
{
Vector2 topLeft = particle.Position * size - new Vector2(triangleSize.X * 0.5f, 0f);
Vector2 topRight = topLeft + new Vector2(triangleSize.X, 0f);
Vector2 bottomLeft = topLeft + new Vector2(0f, triangleSize.Y);
Vector2 bottomRight = topLeft + triangleSize;
Vector2 topLeft = particle.Position - new Vector2(relativeWidth * 0.5f, 0f);
Vector2 topRight = topLeft + new Vector2(relativeWidth, 0f);
Vector2 bottomLeft = topLeft + new Vector2(0f, relativeHeight);
Vector2 bottomRight = bottomLeft + new Vector2(relativeWidth, 0f);
var drawQuad = new Quad(
Vector2Extensions.Transform(topLeft, DrawInfo.Matrix),
Vector2Extensions.Transform(topRight, DrawInfo.Matrix),
Vector2Extensions.Transform(bottomLeft, DrawInfo.Matrix),
Vector2Extensions.Transform(bottomRight, DrawInfo.Matrix)
Vector2Extensions.Transform(topLeft * size, DrawInfo.Matrix),
Vector2Extensions.Transform(topRight * size, DrawInfo.Matrix),
Vector2Extensions.Transform(bottomLeft * size, DrawInfo.Matrix),
Vector2Extensions.Transform(bottomRight * size, DrawInfo.Matrix)
);
var tRect = new Quad(
topLeft.X / size.X,
topLeft.Y / size.Y * texture_height,
texturePartWidth,
texturePartHeight
).AABBFloat;
ColourInfo colourInfo = triangleColourInfo(DrawColourInfo.Colour, new Quad(topLeft, topRight, bottomLeft, bottomRight));
renderer.DrawQuad(texture, drawQuad, DrawColourInfo.Colour, tRect, vertexBatch.AddAction, textureCoords: tRect);
renderer.DrawQuad(texture, drawQuad, colourInfo, vertexAction: vertexBatch.AddAction);
}
shader.Unbind();
}
private static ColourInfo triangleColourInfo(ColourInfo source, Quad quad)
{
return new ColourInfo
{
TopLeft = source.Interpolate(quad.TopLeft),
TopRight = source.Interpolate(quad.TopRight),
BottomLeft = source.Interpolate(quad.BottomLeft),
BottomRight = source.Interpolate(quad.BottomRight)
};
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
@@ -1,14 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterface;
using osuTK;
namespace osu.Game.Graphics.Containers
{
@@ -18,6 +17,12 @@ namespace osu.Game.Graphics.Containers
private readonly Container content = new Container { RelativeSizeAxes = Axes.Both };
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
// base call is checked for cases when `OsuClickableContainer` has masking applied to it directly (ie. externally in object initialisation).
base.ReceivePositionalInputAt(screenSpacePos)
// Implementations often apply masking / edge rounding at a content level, so it's imperative to check that as well.
&& Content.ReceivePositionalInputAt(screenSpacePos);
protected override Container<Drawable> Content => content;
protected virtual HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverClickSounds(sampleSet) { Enabled = { BindTarget = Enabled } };
@@ -38,11 +43,8 @@ namespace osu.Game.Graphics.Containers
content.AutoSizeAxes = AutoSizeAxes;
}
InternalChildren = new Drawable[]
{
content,
CreateHoverSounds(sampleSet)
};
AddInternal(content);
Add(CreateHoverSounds(sampleSet));
}
}
}
@@ -240,7 +240,9 @@ namespace osu.Game.Graphics.Containers
headerBackgroundContainer.Height = expandableHeaderSize + fixedHeaderSize;
headerBackgroundContainer.Y = ExpandableHeader?.Y ?? 0;
float smallestSectionHeight = Children.Count > 0 ? Children.Min(d => d.Height) : 0;
var flowChildren = scrollContentContainer.FlowingChildren.OfType<T>();
float smallestSectionHeight = flowChildren.Any() ? flowChildren.Min(d => d.Height) : 0;
// scroll offset is our fixed header height if we have it plus 10% of content height
// plus 5% to fix floating point errors and to not have a section instantly unselect when scrolling upwards
@@ -249,7 +251,7 @@ namespace osu.Game.Graphics.Containers
float scrollCentre = fixedHeaderSize + scrollContainer.DisplayableContent * scroll_y_centre + selectionLenienceAboveSection;
var presentChildren = Children.Where(c => c.IsPresent);
var presentChildren = flowChildren.Where(c => c.IsPresent);
if (lastClickedSection != null)
SelectedSection.Value = lastClickedSection;
+12 -11
View File
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
@@ -13,6 +11,7 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Graphics.UserInterface
@@ -20,16 +19,12 @@ namespace osu.Game.Graphics.UserInterface
/// <summary>
/// A button with added default sound effects.
/// </summary>
public partial class OsuButton : Button
public abstract partial class OsuButton : Button
{
public LocalisableString Text
{
get => SpriteText?.Text ?? default;
set
{
if (SpriteText != null)
SpriteText.Text = value;
}
get => SpriteText.Text;
set => SpriteText.Text = value;
}
private Color4? backgroundColour;
@@ -66,13 +61,19 @@ namespace osu.Game.Graphics.UserInterface
protected override Container<Drawable> Content { get; }
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
// base call is checked for cases when `OsuClickableContainer` has masking applied to it directly (ie. externally in object initialisation).
base.ReceivePositionalInputAt(screenSpacePos)
// Implementations often apply masking / edge rounding at a content level, so it's imperative to check that as well.
&& Content.ReceivePositionalInputAt(screenSpacePos);
protected Box Hover;
protected Box Background;
protected SpriteText SpriteText;
private readonly Box flashLayer;
public OsuButton(HoverSampleSet? hoverSounds = HoverSampleSet.Button)
protected OsuButton(HoverSampleSet? hoverSounds = HoverSampleSet.Button)
{
Height = 40;
@@ -115,7 +116,7 @@ namespace osu.Game.Graphics.UserInterface
});
if (hoverSounds.HasValue)
AddInternal(new HoverClickSounds(hoverSounds.Value) { Enabled = { BindTarget = Enabled } });
Add(new HoverClickSounds(hoverSounds.Value) { Enabled = { BindTarget = Enabled } });
}
[BackgroundDependencyLoader]
@@ -0,0 +1,212 @@
// 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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Graphics.UserInterface
{
public partial class RangeSlider : CompositeDrawable
{
/// <summary>
/// The lower limiting value
/// </summary>
public Bindable<double> LowerBound
{
get => lowerBound.Current;
set => lowerBound.Current = value;
}
/// <summary>
/// The upper limiting value
/// </summary>
public Bindable<double> UpperBound
{
get => upperBound.Current;
set => upperBound.Current = value;
}
/// <summary>
/// Text that describes this RangeSlider's functionality
/// </summary>
public string Label
{
set => label.Text = value;
}
public float NubWidth
{
set => lowerBound.NubWidth = upperBound.NubWidth = value;
}
/// <summary>
/// Minimum difference between the lower bound and higher bound
/// </summary>
public float MinRange
{
set => minRange = value;
}
/// <summary>
/// lower bound display for when it is set to its default value
/// </summary>
public string DefaultStringLowerBound
{
set => lowerBound.DefaultString = value;
}
/// <summary>
/// upper bound display for when it is set to its default value
/// </summary>
public string DefaultStringUpperBound
{
set => upperBound.DefaultString = value;
}
public LocalisableString DefaultTooltipLowerBound
{
set => lowerBound.DefaultTooltip = value;
}
public LocalisableString DefaultTooltipUpperBound
{
set => upperBound.DefaultTooltip = value;
}
public string TooltipSuffix
{
set => upperBound.TooltipSuffix = lowerBound.TooltipSuffix = value;
}
private float minRange = 0.1f;
private readonly OsuSpriteText label;
private readonly LowerBoundSlider lowerBound;
private readonly UpperBoundSlider upperBound;
public RangeSlider()
{
const float vertical_offset = 13;
InternalChildren = new Drawable[]
{
label = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 14),
},
upperBound = new UpperBoundSlider
{
KeyboardStep = 0.1f,
RelativeSizeAxes = Axes.X,
Y = vertical_offset,
},
lowerBound = new LowerBoundSlider
{
KeyboardStep = 0.1f,
RelativeSizeAxes = Axes.X,
Y = vertical_offset,
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
lowerBound.Current.ValueChanged += min => upperBound.Current.Value = Math.Max(min.NewValue + minRange, upperBound.Current.Value);
upperBound.Current.ValueChanged += max => lowerBound.Current.Value = Math.Min(max.NewValue - minRange, lowerBound.Current.Value);
}
private partial class LowerBoundSlider : BoundSlider
{
protected override void LoadComplete()
{
base.LoadComplete();
LeftBox.Height = 6; // hide any colour bleeding from overlap
AccentColour = BackgroundColour;
BackgroundColour = Color4.Transparent;
}
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
base.ReceivePositionalInputAt(screenSpacePos)
&& screenSpacePos.X <= Nub.ScreenSpaceDrawQuad.TopRight.X;
}
private partial class UpperBoundSlider : BoundSlider
{
protected override void LoadComplete()
{
base.LoadComplete();
RightBox.Height = 6; // just to match the left bar height really
}
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
base.ReceivePositionalInputAt(screenSpacePos)
&& screenSpacePos.X >= Nub.ScreenSpaceDrawQuad.TopLeft.X;
}
protected partial class BoundSlider : OsuSliderBar<double>
{
public string? DefaultString;
public LocalisableString? DefaultTooltip;
public string? TooltipSuffix;
public float NubWidth { get; set; } = Nub.HEIGHT;
public override LocalisableString TooltipText =>
(Current.IsDefault ? DefaultTooltip : Current.Value.ToString($@"0.## {TooltipSuffix}")) ?? Current.Value.ToString($@"0.## {TooltipSuffix}");
protected override bool OnHover(HoverEvent e)
{
base.OnHover(e);
return true; // Make sure only one nub shows hover effect at once.
}
protected override void LoadComplete()
{
base.LoadComplete();
Nub.Width = NubWidth;
RangePadding = Nub.Width / 2;
OsuSpriteText currentDisplay;
Nub.Add(currentDisplay = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Y = -0.5f,
Colour = Color4.White,
Font = OsuFont.Torus.With(size: 10),
});
Current.BindValueChanged(current =>
{
currentDisplay.Text = (current.NewValue != Current.Default ? current.NewValue.ToString("N1") : DefaultString) ?? current.NewValue.ToString("N1");
}, true);
}
[BackgroundDependencyLoader(true)]
private void load(OverlayColourProvider? colourProvider)
{
if (colourProvider == null) return;
AccentColour = colourProvider.Background2;
Nub.AccentColour = colourProvider.Background2;
Nub.GlowingAccentColour = colourProvider.Background1;
Nub.GlowColour = colourProvider.Background2;
}
}
}
}
@@ -6,6 +6,7 @@ using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
@@ -79,8 +80,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
Debug.Assert(triangleGradientSecondColour != null);
Triangles.ColourTop = triangleGradientSecondColour.Value;
Triangles.ColourBottom = BackgroundColour;
Triangles.Colour = ColourInfo.GradientVertical(triangleGradientSecondColour.Value, BackgroundColour);
}
protected override bool OnHover(HoverEvent e)
@@ -25,6 +25,7 @@ namespace osu.Game.Online.API.Requests
var req = base.CreateWebRequest();
if (channel != null) req.AddParameter(@"channel", channel.Id.ToString());
req.AddParameter(@"since", since.ToString());
req.AddParameter(@"includes[]", "presence");
return req;
}
@@ -16,5 +16,7 @@ namespace osu.Game.Online.API.Requests
[JsonProperty]
public List<Message> Messages;
// TODO: Handle Silences here (will need to add to includes[] in the request).
}
}
@@ -32,6 +32,7 @@ namespace osu.Game.Online.API.Requests
Loved,
Pending,
Guest,
Graveyard
Graveyard,
Nominated,
}
}
@@ -164,6 +164,9 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty(@"guest_beatmapset_count")]
public int GuestBeatmapsetCount;
[JsonProperty(@"nominated_beatmapset_count")]
public int NominatedBeatmapsetCount;
[JsonProperty(@"scores_best_count")]
public int ScoresBestCount;
+4
View File
@@ -529,6 +529,10 @@ namespace osu.Game.Online.Chat
{
Logger.Log($"Joined public channel {channel}");
joinChannel(channel, fetchInitialMessages);
// Required after joining public channels to mark the user as online in them.
// Todo: Temporary workaround for https://github.com/ppy/osu-web/issues/9602
SendAck();
};
req.Failure += e =>
{
-4
View File
@@ -1,9 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Online.Chat
@@ -13,7 +10,6 @@ namespace osu.Game.Online.Chat
public InfoMessage(string message)
: base(null)
{
Timestamp = DateTimeOffset.Now;
Content = message;
Sender = APIUser.SYSTEM_USER;
-2
View File
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
namespace osu.Game.Online.Chat
{
public class LocalEchoMessage : LocalMessage
+2 -1
View File
@@ -1,7 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
namespace osu.Game.Online.Chat
{
@@ -13,6 +13,7 @@ namespace osu.Game.Online.Chat
protected LocalMessage(long? id)
: base(id)
{
Timestamp = DateTimeOffset.Now;
}
}
}
+16 -6
View File
@@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.Threading;
using Newtonsoft.Json;
using osu.Game.Online.API.Requests.Responses;
@@ -59,19 +60,28 @@ namespace osu.Game.Online.Chat
/// <remarks>The <see cref="Link"/>s' <see cref="Link.Index"/> and <see cref="Link.Length"/>s are according to <see cref="DisplayContent"/></remarks>
public List<Link> Links;
private static long constructionOrderStatic;
private readonly long constructionOrder;
public Message(long? id)
{
Id = id;
constructionOrder = Interlocked.Increment(ref constructionOrderStatic);
}
public int CompareTo(Message other)
{
if (!Id.HasValue)
return other.Id.HasValue ? 1 : Timestamp.CompareTo(other.Timestamp);
if (!other.Id.HasValue)
return -1;
if (Id.HasValue && other.Id.HasValue)
return Id.Value.CompareTo(other.Id.Value);
return Id.Value.CompareTo(other.Id.Value);
int timestampComparison = Timestamp.CompareTo(other.Timestamp);
if (timestampComparison != 0)
return timestampComparison;
// Timestamp might not be accurate enough to make a stable sorting decision.
return constructionOrder.CompareTo(other.constructionOrder);
}
public virtual bool Equals(Message other)
@@ -85,6 +95,6 @@ namespace osu.Game.Online.Chat
// ReSharper disable once ImpureMethodCallOnReadonlyValueField
public override int GetHashCode() => Id.GetHashCode();
public override string ToString() => $"[{ChannelId}] ({Id}) {Sender}: {Content}";
public override string ToString() => $"({(Id?.ToString() ?? "null")}) {Timestamp} {Sender}: {Content}";
}
}
+67 -9
View File
@@ -1,13 +1,17 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Collections.Generic;
using System.Linq;
using System.Text;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Users;
namespace osu.Game.Online.Chat
@@ -15,21 +19,30 @@ namespace osu.Game.Online.Chat
public partial class NowPlayingCommand : Component
{
[Resolved]
private IChannelPostTarget channelManager { get; set; }
private IChannelPostTarget channelManager { get; set; } = null!;
[Resolved]
private IAPIProvider api { get; set; }
private IAPIProvider api { get; set; } = null!;
[Resolved]
private Bindable<WorkingBeatmap> currentBeatmap { get; set; }
private Bindable<WorkingBeatmap> currentBeatmap { get; set; } = null!;
private readonly Channel target;
[Resolved]
private Bindable<IReadOnlyList<Mod>> selectedMods { get; set; } = null!;
[Resolved]
private IBindable<RulesetInfo> currentRuleset { get; set; } = null!;
[Resolved]
private LocalisationManager localisation { get; set; } = null!;
private readonly Channel? target;
/// <summary>
/// Creates a new <see cref="NowPlayingCommand"/> to post the currently-playing beatmap to a parenting <see cref="IChannelPostTarget"/>.
/// </summary>
/// <param name="target">The target channel to post to. If <c>null</c>, the currently-selected channel will be posted to.</param>
public NowPlayingCommand(Channel target = null)
public NowPlayingCommand(Channel target)
{
this.target = target;
}
@@ -59,10 +72,55 @@ namespace osu.Game.Online.Chat
break;
}
string beatmapString = beatmapInfo.OnlineID > 0 ? $"[{api.WebsiteRootUrl}/b/{beatmapInfo.OnlineID} {beatmapInfo}]" : beatmapInfo.ToString();
string[] pieces =
{
"is",
verb,
getBeatmapPart(),
getRulesetPart(),
getModPart(),
};
channelManager.PostMessage($"is {verb} {beatmapString}", true, target);
channelManager.PostMessage(string.Join(' ', pieces.Where(p => !string.IsNullOrEmpty(p))), true, target);
Expire();
string getBeatmapPart()
{
string beatmapInfoString = localisation.GetLocalisedBindableString(beatmapInfo.GetDisplayTitleRomanisable()).Value;
return beatmapInfo.OnlineID > 0 ? $"[{api.WebsiteRootUrl}/b/{beatmapInfo.OnlineID} {beatmapInfoString}]" : beatmapInfoString;
}
string getRulesetPart()
{
if (api.Activity.Value is not UserActivity.InGame) return string.Empty;
return $"<{currentRuleset.Value.Name}>";
}
string getModPart()
{
if (api.Activity.Value is not UserActivity.InGame) return string.Empty;
if (selectedMods.Value.Count == 0)
{
return string.Empty;
}
StringBuilder modsString = new StringBuilder();
foreach (var mod in selectedMods.Value.Where(mod => mod.Type == ModType.DifficultyIncrease))
{
modsString.Append($"+{mod.Acronym} ");
}
foreach (var mod in selectedMods.Value.Where(mod => mod.Type != ModType.DifficultyIncrease))
{
modsString.Append($"-{mod.Acronym} ");
}
return modsString.ToString().Trim();
}
}
}
}
@@ -192,7 +192,7 @@ namespace osu.Game.Online.Chat
protected partial class StandAloneMessage : ChatLine
{
protected override float TextSize => 15;
protected override float FontSize => 15;
protected override float Spacing => 5;
protected override float UsernameWidth => 75;
@@ -33,11 +33,11 @@ namespace osu.Game.Online.Notifications
public override Task ConnectAsync(CancellationToken cancellationToken)
{
API.Queue(CreateFetchMessagesRequest(0));
API.Queue(CreateInitialFetchRequest(0));
return Task.CompletedTask;
}
protected APIRequest CreateFetchMessagesRequest(long? lastMessageId = null)
protected APIRequest CreateInitialFetchRequest(long? lastMessageId = null)
{
var fetchReq = new GetUpdatesRequest(lastMessageId ?? this.lastMessageId);
@@ -67,8 +67,11 @@ namespace osu.Game.Online.Notifications
protected void HandleChannelParted(Channel channel) => ChannelParted?.Invoke(channel);
protected void HandleMessages(List<Message> messages)
protected void HandleMessages(List<Message>? messages)
{
if (messages == null)
return;
NewMessages?.Invoke(messages);
lastMessageId = Math.Max(lastMessageId, messages.LastOrDefault()?.Id ?? 0);
}
@@ -150,7 +150,7 @@ namespace osu.Game.Online
await disconnect(true);
if (ex != null)
await handleErrorAndDelay(ex, cancellationToken).ConfigureAwait(false);
await handleErrorAndDelay(ex, CancellationToken.None).ConfigureAwait(false);
else
Logger.Log($"{ClientName} disconnected", LoggingTarget.Network);
-2
View File
@@ -7,8 +7,6 @@ using Newtonsoft.Json;
namespace osu.Game.Online.Rooms
{
// TODO: Remove disable below after merging https://github.com/ppy/osu-framework/pull/5548 and applying follow-up changes game-side.
// ReSharper disable once PartialTypeWithSinglePart
public partial class APICreatedRoom : Room
{
[JsonProperty("error")]
+1 -1
View File
@@ -16,7 +16,7 @@ using osu.Game.Online.Rooms.RoomStatuses;
namespace osu.Game.Online.Rooms
{
[JsonObject(MemberSerialization.OptIn)]
public partial class Room
public partial class Room : IDependencyInjectionCandidate
{
[Cached]
[JsonProperty("id")]
@@ -15,8 +15,9 @@ namespace osu.Game.Online.Spectator
/// <summary>
/// Signal the start of a new play session.
/// </summary>
/// <param name="scoreToken">The score submission token.</param>
/// <param name="state">The state of gameplay.</param>
Task BeginPlaySession(SpectatorState state);
Task BeginPlaySession(long? scoreToken, SpectatorState state);
/// <summary>
/// Send a bundle of frame data for the current play session.
@@ -47,7 +47,7 @@ namespace osu.Game.Online.Spectator
}
}
protected override async Task BeginPlayingInternal(SpectatorState state)
protected override async Task BeginPlayingInternal(long? scoreToken, SpectatorState state)
{
if (!IsConnected.Value)
return;
@@ -56,7 +56,7 @@ namespace osu.Game.Online.Spectator
try
{
await connection.InvokeAsync(nameof(ISpectatorServer.BeginPlaySession), state);
await connection.InvokeAsync(nameof(ISpectatorServer.BeginPlaySession), scoreToken, state);
}
catch (Exception exception)
{
@@ -65,7 +65,7 @@ namespace osu.Game.Online.Spectator
Debug.Assert(connector != null);
await connector.Reconnect();
await BeginPlayingInternal(state);
await BeginPlayingInternal(scoreToken, state);
}
// Exceptions can occur if, for instance, the locally played beatmap doesn't have a server-side counterpart.
+7 -5
View File
@@ -76,6 +76,7 @@ namespace osu.Game.Online.Spectator
private IBeatmap? currentBeatmap;
private Score? currentScore;
private long? currentScoreToken;
private readonly Queue<FrameDataBundle> pendingFrameBundles = new Queue<FrameDataBundle>();
@@ -108,7 +109,7 @@ namespace osu.Game.Online.Spectator
// re-send state in case it wasn't received
if (IsPlaying)
// TODO: this is likely sent out of order after a reconnect scenario. needs further consideration.
BeginPlayingInternal(currentState);
BeginPlayingInternal(currentScoreToken, currentState);
}
else
{
@@ -159,7 +160,7 @@ namespace osu.Game.Online.Spectator
return Task.CompletedTask;
}
public void BeginPlaying(GameplayState state, Score score)
public void BeginPlaying(long? scoreToken, GameplayState state, Score score)
{
// This schedule is only here to match the one below in `EndPlaying`.
Schedule(() =>
@@ -174,12 +175,13 @@ namespace osu.Game.Online.Spectator
currentState.RulesetID = score.ScoreInfo.RulesetID;
currentState.Mods = score.ScoreInfo.Mods.Select(m => new APIMod(m)).ToArray();
currentState.State = SpectatedUserState.Playing;
currentState.MaximumScoringValues = state.ScoreProcessor.MaximumScoringValues;
currentState.MaximumStatistics = state.ScoreProcessor.MaximumStatistics;
currentBeatmap = state.Beatmap;
currentScore = score;
currentScoreToken = scoreToken;
BeginPlayingInternal(currentState);
BeginPlayingInternal(currentScoreToken, currentState);
});
}
@@ -264,7 +266,7 @@ namespace osu.Game.Online.Spectator
});
}
protected abstract Task BeginPlayingInternal(SpectatorState state);
protected abstract Task BeginPlayingInternal(long? scoreToken, SpectatorState state);
protected abstract Task SendFramesInternal(FrameDataBundle bundle);
@@ -152,12 +152,12 @@ namespace osu.Game.Online.Spectator
scoreInfo.MaxCombo = frame.Header.MaxCombo;
scoreInfo.Statistics = frame.Header.Statistics;
scoreInfo.MaximumStatistics = spectatorState.MaximumStatistics;
Accuracy.Value = frame.Header.Accuracy;
Combo.Value = frame.Header.Combo;
scoreProcessor.ExtractScoringValues(frame.Header, out var currentScoringValues, out _);
TotalScore.Value = scoreProcessor.ComputeScore(Mode.Value, currentScoringValues, spectatorState.MaximumScoringValues);
TotalScore.Value = scoreProcessor.ComputeScore(Mode.Value, scoreInfo);
}
protected override void Dispose(bool isDisposing)
+2 -2
View File
@@ -9,7 +9,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using MessagePack;
using osu.Game.Online.API;
using osu.Game.Scoring;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Online.Spectator
{
@@ -31,7 +31,7 @@ namespace osu.Game.Online.Spectator
public SpectatedUserState State { get; set; }
[Key(4)]
public ScoringValues MaximumScoringValues { get; set; }
public Dictionary<HitResult, int> MaximumStatistics { get; set; } = new Dictionary<HitResult, int>();
public bool Equals(SpectatorState other)
{
@@ -68,11 +68,15 @@ namespace osu.Game.Overlays.Changelog
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Direction = FillDirection.Vertical,
Margin = new MarginPadding { Top = 20 },
Children = new Drawable[]
Child = new FillFlowContainer
{
new OsuHoverContainer
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Child = new OsuHoverContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -4,7 +4,6 @@
#nullable disable
using System;
using System.Linq;
using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
@@ -104,27 +103,29 @@ namespace osu.Game.Overlays.Changelog
{
var fill = base.CreateHeader();
foreach (var existing in fill.Children.OfType<OsuHoverContainer>())
var nestedFill = (FillFlowContainer)fill.Child;
var buildDisplay = (OsuHoverContainer)nestedFill.Child;
buildDisplay.Scale = new Vector2(1.25f);
buildDisplay.Action = null;
fill.Add(date = new OsuSpriteText
{
existing.Scale = new Vector2(1.25f);
existing.Action = null;
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = Build.CreatedAt.Date.ToString("dd MMMM yyyy"),
Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 14),
Margin = new MarginPadding { Top = 5 },
Scale = new Vector2(1.25f),
});
existing.Add(date = new OsuSpriteText
{
Text = Build.CreatedAt.Date.ToString("dd MMMM yyyy"),
Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 14),
Anchor = Anchor.BottomCentre,
Origin = Anchor.TopCentre,
Margin = new MarginPadding { Top = 5 },
});
}
fill.Insert(-1, new NavigationIconButton(Build.Versions?.Previous)
nestedFill.Insert(-1, new NavigationIconButton(Build.Versions?.Previous)
{
Icon = FontAwesome.Solid.ChevronLeft,
SelectBuild = b => SelectBuild(b)
});
fill.Insert(1, new NavigationIconButton(Build.Versions?.Next)
nestedFill.Insert(1, new NavigationIconButton(Build.Versions?.Next)
{
Icon = FontAwesome.Solid.ChevronRight,
SelectBuild = b => SelectBuild(b)
+43 -195
View File
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using System.Collections.Generic;
using osu.Framework.Allocation;
@@ -9,25 +8,20 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat;
using osuTK;
using osuTK.Graphics;
using osu.Framework.Graphics.Sprites;
namespace osu.Game.Overlays.Chat
{
public partial class ChatLine : CompositeDrawable
{
private Message message = null!;
public Message Message
{
get => message;
@@ -44,49 +38,35 @@ namespace osu.Game.Overlays.Chat
}
}
public LinkFlowContainer ContentFlow { get; private set; } = null!;
public IReadOnlyCollection<Drawable> DrawableContentFlow => drawableContentFlow;
protected virtual float TextSize => 20;
protected virtual float FontSize => 20;
protected virtual float Spacing => 15;
protected virtual float UsernameWidth => 130;
private Color4 usernameColour;
private OsuSpriteText timestamp = null!;
private Message message = null!;
private OsuSpriteText username = null!;
private Container? highlight;
private readonly Bindable<bool> prefer24HourTime = new Bindable<bool>();
private bool senderHasColour => !string.IsNullOrEmpty(message.Sender.Colour);
private bool messageHasColour => Message.IsAction && senderHasColour;
[Resolved]
private ChannelManager? chatManager { get; set; }
[Resolved]
private OsuColour colours { get; set; } = null!;
private OverlayColourProvider? colourProvider { get; set; }
private readonly OsuSpriteText drawableTimestamp;
private readonly DrawableUsername drawableUsername;
private readonly LinkFlowContainer drawableContentFlow;
private readonly Bindable<bool> prefer24HourTime = new Bindable<bool>();
private Container? highlight;
public ChatLine(Message message)
{
Message = message;
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider? colourProvider, OsuConfigManager configManager)
{
usernameColour = senderHasColour
? Color4Extensions.FromHex(message.Sender.Colour)
: username_colours[message.Sender.Id % username_colours.Length];
InternalChild = new GridContainer
{
@@ -103,30 +83,24 @@ namespace osu.Game.Overlays.Chat
{
new Drawable[]
{
timestamp = new OsuSpriteText
drawableTimestamp = new OsuSpriteText
{
Shadow = false,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Font = OsuFont.GetFont(size: TextSize * 0.75f, weight: FontWeight.SemiBold, fixedWidth: true),
Colour = colourProvider?.Background1 ?? Colour4.White,
Font = OsuFont.GetFont(size: FontSize * 0.75f, weight: FontWeight.SemiBold, fixedWidth: true),
AlwaysPresent = true,
},
new MessageSender(message.Sender)
drawableUsername = new DrawableUsername(message.Sender)
{
Width = UsernameWidth,
FontSize = FontSize,
AutoSizeAxes = Axes.Y,
Origin = Anchor.TopRight,
Anchor = Anchor.TopRight,
Child = createUsername(),
Margin = new MarginPadding { Horizontal = Spacing },
},
ContentFlow = new LinkFlowContainer(t =>
{
t.Shadow = false;
t.Font = t.Font.With(size: TextSize, italics: Message.IsAction);
t.Colour = messageHasColour ? Color4Extensions.FromHex(message.Sender.Colour) : colourProvider?.Content1 ?? Colour4.White;
})
drawableContentFlow = new LinkFlowContainer(styleMessageContent)
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
@@ -134,18 +108,23 @@ namespace osu.Game.Overlays.Chat
},
}
};
}
[BackgroundDependencyLoader]
private void load(OsuConfigManager configManager)
{
configManager.BindWith(OsuSetting.Prefer24HourTime, prefer24HourTime);
prefer24HourTime.BindValueChanged(_ => updateTimestamp());
}
protected override void LoadComplete()
{
base.LoadComplete();
drawableTimestamp.Colour = colourProvider?.Background1 ?? Colour4.White;
updateMessageContent();
FinishTransforms(true);
prefer24HourTime.BindValueChanged(_ => updateTimestamp());
}
/// <summary>
@@ -160,7 +139,7 @@ namespace osu.Game.Overlays.Chat
CornerRadius = 2f,
Masking = true,
RelativeSizeAxes = Axes.Both,
Colour = usernameColour.Darken(1f),
Colour = drawableUsername.AccentColour.Darken(1f),
Depth = float.MaxValue,
Child = new Box { RelativeSizeAxes = Axes.Both }
});
@@ -170,166 +149,35 @@ namespace osu.Game.Overlays.Chat
highlight.Expire();
}
private void styleMessageContent(SpriteText text)
{
text.Shadow = false;
text.Font = text.Font.With(size: FontSize, italics: Message.IsAction);
bool messageHasColour = Message.IsAction && !string.IsNullOrEmpty(message.Sender.Colour);
text.Colour = messageHasColour ? Color4Extensions.FromHex(message.Sender.Colour) : colourProvider?.Content1 ?? Colour4.White;
}
private void updateMessageContent()
{
this.FadeTo(message is LocalEchoMessage ? 0.4f : 1.0f, 500, Easing.OutQuint);
timestamp.FadeTo(message is LocalEchoMessage ? 0 : 1, 500, Easing.OutQuint);
drawableTimestamp.FadeTo(message is LocalEchoMessage ? 0 : 1, 500, Easing.OutQuint);
updateTimestamp();
username.Text = $@"{message.Sender.Username}";
drawableUsername.Text = $@"{message.Sender.Username}";
// remove non-existent channels from the link list
message.Links.RemoveAll(link => link.Action == LinkAction.OpenChannel && chatManager?.AvailableChannels.Any(c => c.Name == link.Argument.ToString()) != true);
ContentFlow.Clear();
ContentFlow.AddLinks(message.DisplayContent, message.Links);
drawableContentFlow.Clear();
drawableContentFlow.AddLinks(message.DisplayContent, message.Links);
}
private void updateTimestamp()
{
timestamp.Text = prefer24HourTime.Value
drawableTimestamp.Text = prefer24HourTime.Value
? $@"{message.Timestamp.LocalDateTime:HH:mm:ss}"
: $@"{message.Timestamp.LocalDateTime:hh:mm:ss tt}";
}
private Drawable createUsername()
{
username = new OsuSpriteText
{
Shadow = false,
Colour = senderHasColour ? colours.ChatBlue : usernameColour,
Truncate = true,
EllipsisString = "…",
Font = OsuFont.GetFont(size: TextSize, weight: FontWeight.Bold, italics: true),
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
MaxWidth = UsernameWidth,
};
if (!senderHasColour)
return username;
// Background effect
return new Container
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
AutoSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 4,
EdgeEffect = new EdgeEffectParameters
{
Roundness = 1,
Radius = 1,
Colour = Color4.Black.Opacity(0.3f),
Offset = new Vector2(0, 1),
Type = EdgeEffectType.Shadow,
},
Child = new Container
{
AutoSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 4,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = usernameColour,
},
new Container
{
AutoSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = 4, Right = 4, Bottom = 1, Top = -2 },
Child = username
}
}
}
};
}
private partial class MessageSender : OsuClickableContainer, IHasContextMenu
{
private readonly APIUser sender;
private Action startChatAction = null!;
[Resolved]
private IAPIProvider api { get; set; } = null!;
public MessageSender(APIUser sender)
{
this.sender = sender;
}
[BackgroundDependencyLoader]
private void load(UserProfileOverlay? profile, ChannelManager? chatManager, ChatOverlay? chatOverlay)
{
Action = () => profile?.ShowUser(sender);
startChatAction = () =>
{
chatManager?.OpenPrivateChannel(sender);
chatOverlay?.Show();
};
}
public MenuItem[] ContextMenuItems
{
get
{
if (sender.Equals(APIUser.SYSTEM_USER))
return Array.Empty<MenuItem>();
List<MenuItem> items = new List<MenuItem>
{
new OsuMenuItem("View Profile", MenuItemType.Highlighted, Action)
};
if (!sender.Equals(api.LocalUser.Value))
items.Add(new OsuMenuItem("Start Chat", MenuItemType.Standard, startChatAction));
return items.ToArray();
}
}
}
private static readonly Color4[] username_colours =
{
Color4Extensions.FromHex("588c7e"),
Color4Extensions.FromHex("b2a367"),
Color4Extensions.FromHex("c98f65"),
Color4Extensions.FromHex("bc5151"),
Color4Extensions.FromHex("5c8bd6"),
Color4Extensions.FromHex("7f6ab7"),
Color4Extensions.FromHex("a368ad"),
Color4Extensions.FromHex("aa6880"),
Color4Extensions.FromHex("6fad9b"),
Color4Extensions.FromHex("f2e394"),
Color4Extensions.FromHex("f2ae72"),
Color4Extensions.FromHex("f98f8a"),
Color4Extensions.FromHex("7daef4"),
Color4Extensions.FromHex("a691f2"),
Color4Extensions.FromHex("c894d3"),
Color4Extensions.FromHex("d895b0"),
Color4Extensions.FromHex("53c4a1"),
Color4Extensions.FromHex("eace5c"),
Color4Extensions.FromHex("ea8c47"),
Color4Extensions.FromHex("fc4f4f"),
Color4Extensions.FromHex("3d94ea"),
Color4Extensions.FromHex("7760ea"),
Color4Extensions.FromHex("af52c6"),
Color4Extensions.FromHex("e25696"),
Color4Extensions.FromHex("677c66"),
Color4Extensions.FromHex("9b8732"),
Color4Extensions.FromHex("8c5129"),
Color4Extensions.FromHex("8c3030"),
Color4Extensions.FromHex("1f5d91"),
Color4Extensions.FromHex("4335a5"),
Color4Extensions.FromHex("812a96"),
Color4Extensions.FromHex("992861"),
};
}
}
+225
View File
@@ -0,0 +1,225 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.Chat
{
public partial class DrawableUsername : OsuClickableContainer, IHasContextMenu
{
public Color4 AccentColour { get; }
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
colouredDrawable.ReceivePositionalInputAt(screenSpacePos);
public float FontSize
{
set => drawableText.Font = OsuFont.GetFont(size: value, weight: FontWeight.Bold, italics: true);
}
public LocalisableString Text
{
set => drawableText.Text = value;
}
public override float Width
{
get => base.Width;
set => base.Width = drawableText.MaxWidth = value;
}
[Resolved]
private IAPIProvider api { get; set; } = null!;
[Resolved]
private OsuColour colours { get; set; } = null!;
[Resolved(canBeNull: true)]
private ChannelManager? chatManager { get; set; }
[Resolved(canBeNull: true)]
private ChatOverlay? chatOverlay { get; set; }
[Resolved(canBeNull: true)]
private UserProfileOverlay? profileOverlay { get; set; }
private readonly APIUser user;
private readonly OsuSpriteText drawableText;
private readonly Drawable colouredDrawable;
public DrawableUsername(APIUser user)
{
this.user = user;
Action = openUserProfile;
drawableText = new OsuSpriteText
{
Shadow = false,
Truncate = true,
EllipsisString = "…",
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
};
if (string.IsNullOrWhiteSpace(user.Colour))
{
AccentColour = default_colours[user.Id % default_colours.Length];
Add(colouredDrawable = drawableText);
}
else
{
AccentColour = Color4Extensions.FromHex(user.Colour);
Add(new Container
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
AutoSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 4,
EdgeEffect = new EdgeEffectParameters
{
Roundness = 1,
Radius = 1,
Colour = Color4.Black.Opacity(0.3f),
Offset = new Vector2(0, 1),
Type = EdgeEffectType.Shadow,
},
Child = new Container
{
AutoSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 4,
Children = new[]
{
colouredDrawable = new Box
{
RelativeSizeAxes = Axes.Both,
},
new Container
{
AutoSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = 4, Right = 4, Bottom = 1, Top = -2 },
Child = drawableText,
}
}
}
});
}
}
protected override void LoadComplete()
{
base.LoadComplete();
drawableText.Colour = colours.ChatBlue;
colouredDrawable.Colour = AccentColour;
}
public MenuItem[] ContextMenuItems
{
get
{
if (user.Equals(APIUser.SYSTEM_USER))
return Array.Empty<MenuItem>();
List<MenuItem> items = new List<MenuItem>
{
new OsuMenuItem("View Profile", MenuItemType.Highlighted, openUserProfile)
};
if (!user.Equals(api.LocalUser.Value))
items.Add(new OsuMenuItem("Start Chat", MenuItemType.Standard, openUserChannel));
return items.ToArray();
}
}
private void openUserChannel()
{
chatManager?.OpenPrivateChannel(user);
chatOverlay?.Show();
}
private void openUserProfile()
{
profileOverlay?.ShowUser(user);
}
protected override bool OnHover(HoverEvent e)
{
colouredDrawable.FadeColour(AccentColour.Lighten(0.6f), 30, Easing.OutQuint);
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
base.OnHoverLost(e);
colouredDrawable.FadeColour(AccentColour, 800, Easing.OutQuint);
}
private static readonly Color4[] default_colours =
{
Color4Extensions.FromHex("588c7e"),
Color4Extensions.FromHex("b2a367"),
Color4Extensions.FromHex("c98f65"),
Color4Extensions.FromHex("bc5151"),
Color4Extensions.FromHex("5c8bd6"),
Color4Extensions.FromHex("7f6ab7"),
Color4Extensions.FromHex("a368ad"),
Color4Extensions.FromHex("aa6880"),
Color4Extensions.FromHex("6fad9b"),
Color4Extensions.FromHex("f2e394"),
Color4Extensions.FromHex("f2ae72"),
Color4Extensions.FromHex("f98f8a"),
Color4Extensions.FromHex("7daef4"),
Color4Extensions.FromHex("a691f2"),
Color4Extensions.FromHex("c894d3"),
Color4Extensions.FromHex("d895b0"),
Color4Extensions.FromHex("53c4a1"),
Color4Extensions.FromHex("eace5c"),
Color4Extensions.FromHex("ea8c47"),
Color4Extensions.FromHex("fc4f4f"),
Color4Extensions.FromHex("3d94ea"),
Color4Extensions.FromHex("7760ea"),
Color4Extensions.FromHex("af52c6"),
Color4Extensions.FromHex("e25696"),
Color4Extensions.FromHex("677c66"),
Color4Extensions.FromHex("9b8732"),
Color4Extensions.FromHex("8c5129"),
Color4Extensions.FromHex("8c3030"),
Color4Extensions.FromHex("1f5d91"),
Color4Extensions.FromHex("4335a5"),
Color4Extensions.FromHex("812a96"),
Color4Extensions.FromHex("992861"),
};
}
}
@@ -81,7 +81,7 @@ namespace osu.Game.Overlays.FirstRunSetup
loading.Hide();
tick.FadeIn(500, Easing.OutQuint);
Background.FadeColour(colours.Green, 500, Easing.OutQuint);
this.TransformTo(nameof(BackgroundColour), colours.Green, 500, Easing.OutQuint);
progressBar.FillColour = colours.Green;
this.TransformBindableTo(progressBar.Current, 1, 500, Easing.OutQuint);
@@ -233,7 +233,7 @@ namespace osu.Game.Overlays.FirstRunSetup
return parentDependencies.Get(type, info);
}
public void Inject<T>(T instance) where T : class
public void Inject<T>(T instance) where T : class, IDependencyInjectionCandidate
{
parentDependencies.Inject(instance);
}
@@ -58,6 +58,9 @@ namespace osu.Game.Overlays.Profile.Sections.Beatmaps
case BeatmapSetType.Guest:
return user.GuestBeatmapsetCount;
case BeatmapSetType.Nominated:
return user.NominatedBeatmapsetCount;
default:
return 0;
}
@@ -25,7 +25,8 @@ namespace osu.Game.Overlays.Profile.Sections
new PaginatedBeatmapContainer(BeatmapSetType.Loved, User, UsersStrings.ShowExtraBeatmapsLovedTitle),
new PaginatedBeatmapContainer(BeatmapSetType.Guest, User, UsersStrings.ShowExtraBeatmapsGuestTitle),
new PaginatedBeatmapContainer(BeatmapSetType.Pending, User, UsersStrings.ShowExtraBeatmapsPendingTitle),
new PaginatedBeatmapContainer(BeatmapSetType.Graveyard, User, UsersStrings.ShowExtraBeatmapsGraveyardTitle)
new PaginatedBeatmapContainer(BeatmapSetType.Graveyard, User, UsersStrings.ShowExtraBeatmapsGraveyardTitle),
new PaginatedBeatmapContainer(BeatmapSetType.Nominated, User, UsersStrings.ShowExtraBeatmapsNominatedTitle),
};
}
}
@@ -177,13 +177,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
updateScreenModeWarning();
}, true);
windowModes.BindCollectionChanged((_, _) =>
{
if (windowModes.Count > 1)
windowModeDropdown.Show();
else
windowModeDropdown.Hide();
}, true);
windowModes.BindCollectionChanged((_, _) => updateDisplaySettingsVisibility());
currentDisplay.BindValueChanged(display => Schedule(() =>
{
@@ -219,7 +213,11 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
scalingSettings.ResizeHeightTo(0, transition_duration, Easing.OutQuint);
scalingSettings.AutoSizeAxes = scalingMode.Value != ScalingMode.Off ? Axes.Y : Axes.None;
scalingSettings.ForEach(s => s.TransferValueOnCommit = scalingMode.Value == ScalingMode.Everything);
scalingSettings.ForEach(s =>
{
s.TransferValueOnCommit = scalingMode.Value == ScalingMode.Everything;
s.CanBeShown.Value = scalingMode.Value != ScalingMode.Off;
});
}
}
@@ -234,20 +232,10 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
private void updateDisplaySettingsVisibility()
{
if (resolutions.Count > 1 && windowModeDropdown.Current.Value == WindowMode.Fullscreen)
resolutionDropdown.Show();
else
resolutionDropdown.Hide();
if (displayDropdown.Items.Count() > 1)
displayDropdown.Show();
else
displayDropdown.Hide();
if (host.Window?.SafeAreaPadding.Value.Total != Vector2.Zero)
safeAreaConsiderationsCheckbox.Show();
else
safeAreaConsiderationsCheckbox.Hide();
windowModeDropdown.CanBeShown.Value = windowModes.Count > 1;
resolutionDropdown.CanBeShown.Value = resolutions.Count > 1 && windowModeDropdown.Current.Value == WindowMode.Fullscreen;
displayDropdown.CanBeShown.Value = displayDropdown.Items.Count() > 1;
safeAreaConsiderationsCheckbox.CanBeShown.Value = host.Window?.SafeAreaPadding.Value.Total != Vector2.Zero;
}
private void updateScreenModeWarning()
@@ -143,6 +143,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
areaOffset.SetDefault();
areaSize.SetDefault();
},
CanBeShown = { BindTarget = enabled }
},
new SettingsButton
{
@@ -150,25 +151,29 @@ namespace osu.Game.Overlays.Settings.Sections.Input
Action = () =>
{
forceAspectRatio((float)host.Window.ClientSize.Width / host.Window.ClientSize.Height);
}
},
CanBeShown = { BindTarget = enabled }
},
new SettingsSlider<float>
{
TransferValueOnCommit = true,
LabelText = TabletSettingsStrings.XOffset,
Current = offsetX
Current = offsetX,
CanBeShown = { BindTarget = enabled }
},
new SettingsSlider<float>
{
TransferValueOnCommit = true,
LabelText = TabletSettingsStrings.YOffset,
Current = offsetY
Current = offsetY,
CanBeShown = { BindTarget = enabled }
},
new SettingsSlider<float>
{
TransferValueOnCommit = true,
LabelText = TabletSettingsStrings.Rotation,
Current = rotation
Current = rotation,
CanBeShown = { BindTarget = enabled }
},
new RotationPresetButtons(tabletHandler)
{
@@ -181,24 +186,28 @@ namespace osu.Game.Overlays.Settings.Sections.Input
{
TransferValueOnCommit = true,
LabelText = TabletSettingsStrings.AspectRatio,
Current = aspectRatio
Current = aspectRatio,
CanBeShown = { BindTarget = enabled }
},
new SettingsCheckbox
{
LabelText = TabletSettingsStrings.LockAspectRatio,
Current = aspectLock
Current = aspectLock,
CanBeShown = { BindTarget = enabled }
},
new SettingsSlider<float>
{
TransferValueOnCommit = true,
LabelText = CommonStrings.Width,
Current = sizeX
Current = sizeX,
CanBeShown = { BindTarget = enabled }
},
new SettingsSlider<float>
{
TransferValueOnCommit = true,
LabelText = CommonStrings.Height,
Current = sizeY
Current = sizeY,
CanBeShown = { BindTarget = enabled }
},
}
},
+6 -1
View File
@@ -3,14 +3,16 @@
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterfaceV2;
namespace osu.Game.Overlays.Settings
{
public partial class SettingsButton : RoundedButton, IHasTooltip
public partial class SettingsButton : RoundedButton, IHasTooltip, IConditionalFilterable
{
public SettingsButton()
{
@@ -20,6 +22,9 @@ namespace osu.Game.Overlays.Settings
public LocalisableString TooltipText { get; set; }
public BindableBool CanBeShown { get; } = new BindableBool(true);
IBindable<bool> IConditionalFilterable.CanBeShown => CanBeShown;
public override IEnumerable<LocalisableString> FilterTerms
{
get
+4 -1
View File
@@ -22,7 +22,7 @@ using osuTK;
namespace osu.Game.Overlays.Settings
{
public abstract partial class SettingsItem<T> : Container, IFilterable, ISettingsItem, IHasCurrentValue<T>, IHasTooltip
public abstract partial class SettingsItem<T> : Container, IConditionalFilterable, ISettingsItem, IHasCurrentValue<T>, IHasTooltip
{
protected abstract Drawable CreateControl();
@@ -144,6 +144,9 @@ namespace osu.Game.Overlays.Settings
public bool FilteringActive { get; set; }
public BindableBool CanBeShown { get; } = new BindableBool(true);
IBindable<bool> IConditionalFilterable.CanBeShown => CanBeShown;
public event Action SettingChanged;
private T classicDefault;
@@ -5,7 +5,6 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -19,7 +18,7 @@ using osuTK;
namespace osu.Game.Overlays.Settings
{
public abstract partial class SettingsSection : Container, IHasFilterableChildren
public abstract partial class SettingsSection : Container, IFilterable
{
protected FillFlowContainer FlowContent;
protected override Container<Drawable> Content => FlowContent;
@@ -33,7 +32,6 @@ namespace osu.Game.Overlays.Settings
public abstract Drawable CreateIcon();
public abstract LocalisableString Header { get; }
public IEnumerable<IFilterable> FilterableChildren => Children.OfType<IFilterable>();
public virtual IEnumerable<LocalisableString> FilterTerms => new[] { Header };
public const int ITEM_SPACING = 14;
@@ -8,7 +8,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Localisation;
using osu.Framework.Testing;
@@ -17,7 +16,7 @@ using osu.Game.Graphics;
namespace osu.Game.Overlays.Settings
{
[ExcludeFromDynamicCompile]
public abstract partial class SettingsSubsection : FillFlowContainer, IHasFilterableChildren
public abstract partial class SettingsSubsection : FillFlowContainer, IFilterable
{
protected override Container<Drawable> Content => FlowContent;
@@ -25,8 +24,6 @@ namespace osu.Game.Overlays.Settings
protected abstract LocalisableString Header { get; }
public IEnumerable<IFilterable> FilterableChildren => Children.OfType<IFilterable>();
public virtual IEnumerable<LocalisableString> FilterTerms => new[] { Header };
public bool MatchingFilter
+3 -1
View File
@@ -205,7 +205,9 @@ namespace osu.Game.Overlays
protected override UserTrackingScrollContainer CreateScrollContainer() => new OverlayScrollContainer();
protected override FlowContainer<ProfileSection> CreateScrollContentContainer() => new FillFlowContainer<ProfileSection>
// Reverse child ID is required so expanding beatmap panels can appear above sections below them.
// This can also be done by setting Depth when adding new sections above if using ReverseChildID turns out to have any issues.
protected override FlowContainer<ProfileSection> CreateScrollContentContainer() => new ReverseChildIDFillFlowContainer<ProfileSection>
{
Direction = FillDirection.Vertical,
AutoSizeAxes = Axes.Y,
@@ -5,9 +5,11 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Configuration;
using osu.Framework.Extensions;
using osu.Game.Configuration;
using osu.Game.Database;
@@ -67,7 +69,7 @@ namespace osu.Game.Rulesets.Configuration
{
var setting = r.All<RealmRulesetSetting>().First(s => s.RulesetName == rulesetName && s.Variant == variant && s.Key == c.ToString());
setting.Value = ConfigStore[c].ToString();
setting.Value = ConfigStore[c].ToString(CultureInfo.InvariantCulture);
}
});
@@ -89,7 +91,7 @@ namespace osu.Game.Rulesets.Configuration
setting = new RealmRulesetSetting
{
Key = lookup.ToString(),
Value = bindable.Value.ToString(),
Value = bindable.ToString(CultureInfo.InvariantCulture),
RulesetName = rulesetName,
Variant = variant,
};
+5 -4
View File
@@ -45,8 +45,6 @@ namespace osu.Game.Rulesets.Edit
{
protected IRulesetConfigManager Config { get; private set; }
protected readonly Ruleset Ruleset;
// Provides `Playfield`
private DependencyContainer dependencies;
@@ -74,8 +72,8 @@ namespace osu.Game.Rulesets.Edit
private IBindable<bool> hasTiming;
protected HitObjectComposer(Ruleset ruleset)
: base(ruleset)
{
Ruleset = ruleset;
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) =>
@@ -419,8 +417,11 @@ namespace osu.Game.Rulesets.Edit
[Cached]
public abstract partial class HitObjectComposer : CompositeDrawable, IPositionSnapProvider
{
protected HitObjectComposer()
public readonly Ruleset Ruleset;
protected HitObjectComposer(Ruleset ruleset)
{
Ruleset = ruleset;
RelativeSizeAxes = Axes.Both;
}
+3 -5
View File
@@ -1,14 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
namespace osu.Game.Rulesets.Edit
{
public interface IBeatSnapProvider
{
/// <summary>
/// Snaps a duration to the closest beat of a timing point applicable at the reference time.
/// Snaps a duration to the closest beat of a timing point applicable at the reference time, factoring in the current <see cref="BeatDivisor"/>.
/// </summary>
/// <param name="time">The time to snap.</param>
/// <param name="referenceTime">An optional reference point to use for timing point lookup.</param>
@@ -16,10 +14,10 @@ namespace osu.Game.Rulesets.Edit
double SnapTime(double time, double? referenceTime = null);
/// <summary>
/// Get the most appropriate beat length at a given time.
/// Get the most appropriate beat length at a given time, pre-divided by <see cref="BeatDivisor"/>.
/// </summary>
/// <param name="referenceTime">A reference time used for lookup.</param>
/// <returns>The most appropriate beat length.</returns>
/// <returns>The most appropriate beat length, divided by <see cref="BeatDivisor"/>.</returns>
double GetBeatLengthAtTime(double referenceTime);
/// <summary>
+8 -1
View File
@@ -118,11 +118,18 @@ namespace osu.Game.Rulesets.Mods
if (!(them is DifficultyBindable otherDifficultyBindable))
throw new InvalidOperationException($"Cannot bind to a non-{nameof(DifficultyBindable)}.");
// ensure that MaxValue and ExtendedMaxValue are copied across first before continuing.
// not doing so may cause the value of CurrentNumber to be truncated to 10.
otherDifficultyBindable.CopyTo(this);
// set up mutual binding for ExtendedLimits to correctly set the upper bound of CurrentNumber.
ExtendedLimits.BindTarget = otherDifficultyBindable.ExtendedLimits;
// the actual values need to be copied after the max value constraints.
// set up mutual binding for CurrentNumber. this must happen after all of the above.
CurrentNumber.BindTarget = otherDifficultyBindable.CurrentNumber;
// finish up the binding by setting up weak references via the base call.
// unfortunately this will call `.CopyTo()` again, but fixing that is problematic and messy.
base.BindTo(them);
}
+1 -2
View File
@@ -7,7 +7,6 @@ using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Mods
@@ -71,7 +70,7 @@ namespace osu.Game.Rulesets.Mods
SpeedChange.SetDefault();
double firstObjectStart = beatmap.HitObjects.FirstOrDefault()?.StartTime ?? 0;
double lastObjectEnd = beatmap.HitObjects.LastOrDefault()?.GetEndTime() ?? 0;
double lastObjectEnd = beatmap.HitObjects.Any() ? beatmap.GetLastObjectTime() : 0;
beginRampTime = firstObjectStart;
finalRateTime = firstObjectStart + FINAL_RATE_PROGRESS * (lastObjectEnd - firstObjectStart);
@@ -27,11 +27,8 @@ namespace osu.Game.Rulesets.Objects
if (beatmap.HitObjects.Count == 0)
return;
HitObject firstObject = beatmap.HitObjects.First();
HitObject lastObject = beatmap.HitObjects.Last();
double firstHitTime = firstObject.StartTime;
double lastHitTime = 1 + lastObject.GetEndTime();
double firstHitTime = beatmap.HitObjects.First().StartTime;
double lastHitTime = 1 + beatmap.GetLastObjectTime();
var timingPoints = beatmap.ControlPointInfo.TimingPoints;
@@ -199,7 +199,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
comboColourBrightness.BindValueChanged(_ => UpdateComboColour());
// Apply transforms
updateState(State.Value, true);
updateStateFromResult();
}
/// <summary>
@@ -266,12 +266,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
// If not loaded, the state update happens in LoadComplete().
if (IsLoaded)
{
if (Result.IsHit)
updateState(ArmedState.Hit, true);
else if (Result.HasResult)
updateState(ArmedState.Miss, true);
else
updateState(ArmedState.Idle, true);
updateStateFromResult();
// Combo colour may have been applied via a bindable flow while no object entry was attached.
// Update here to ensure we're in a good state.
@@ -279,6 +274,16 @@ namespace osu.Game.Rulesets.Objects.Drawables
}
}
private void updateStateFromResult()
{
if (Result.IsHit)
updateState(ArmedState.Hit, true);
else if (Result.HasResult)
updateState(ArmedState.Miss, true);
else
updateState(ArmedState.Idle, true);
}
protected sealed override void OnFree(HitObjectLifetimeEntry entry)
{
StartTimeBindable.UnbindFrom(HitObject.StartTimeBindable);
+36 -39
View File
@@ -10,7 +10,6 @@ using osu.Framework.Bindables;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Extensions;
using osu.Game.Online.Spectator;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
@@ -90,17 +89,14 @@ namespace osu.Game.Rulesets.Scoring
private readonly double accuracyPortion;
private readonly double comboPortion;
/// <summary>
/// Scoring values for a perfect play.
/// </summary>
public ScoringValues MaximumScoringValues
public Dictionary<HitResult, int> MaximumStatistics
{
get
{
if (!beatmapApplied)
throw new InvalidOperationException($"Cannot access maximum scoring values before calling {nameof(ApplyBeatmap)}.");
throw new InvalidOperationException($"Cannot access maximum statistics before calling {nameof(ApplyBeatmap)}.");
return maximumScoringValues;
return new Dictionary<HitResult, int>(maximumResultCounts);
}
}
@@ -268,7 +264,7 @@ namespace osu.Game.Rulesets.Scoring
private void updateScore()
{
Accuracy.Value = currentMaximumScoringValues.BaseScore > 0 ? (double)currentScoringValues.BaseScore / currentMaximumScoringValues.BaseScore : 1;
TotalScore.Value = ComputeScore(Mode.Value, currentScoringValues, maximumScoringValues);
TotalScore.Value = computeScore(Mode.Value, currentScoringValues, maximumScoringValues);
}
/// <summary>
@@ -285,7 +281,7 @@ namespace osu.Game.Rulesets.Scoring
// We only extract scoring values from the score's statistics. This is because accuracy is always relative to the point of pass or fail rather than relative to the whole beatmap.
extractScoringValues(scoreInfo.Statistics, out var current, out var maximum);
return maximum.BaseScore > 0 ? current.BaseScore / maximum.BaseScore : 1;
return maximum.BaseScore > 0 ? (double)current.BaseScore / maximum.BaseScore : 1;
}
/// <summary>
@@ -303,9 +299,9 @@ namespace osu.Game.Rulesets.Scoring
if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
throw new ArgumentException($"Unexpected score ruleset. Expected \"{ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\".");
ExtractScoringValues(scoreInfo, out var current, out var maximum);
extractScoringValues(scoreInfo, out var current, out var maximum);
return ComputeScore(mode, current, maximum);
return computeScore(mode, current, maximum);
}
/// <summary>
@@ -316,7 +312,7 @@ namespace osu.Game.Rulesets.Scoring
/// <param name="maximum">The maximum scoring values.</param>
/// <returns>The total score computed from the given scoring values.</returns>
[Pure]
public long ComputeScore(ScoringMode mode, ScoringValues current, ScoringValues maximum)
private long computeScore(ScoringMode mode, ScoringValues current, ScoringValues maximum)
{
double accuracyRatio = maximum.BaseScore > 0 ? (double)current.BaseScore / maximum.BaseScore : 1;
double comboRatio = maximum.MaxCombo > 0 ? (double)current.MaxCombo / maximum.MaxCombo : 1;
@@ -474,14 +470,14 @@ namespace osu.Game.Rulesets.Scoring
/// Consumers are expected to more accurately fill in the above values through external means.
/// <para>
/// <b>Ensure</b> to fill in the maximum <see cref="ScoringValues.CountBasicHitObjects"/> for use in
/// <see cref="ComputeScore(osu.Game.Rulesets.Scoring.ScoringMode,osu.Game.Scoring.ScoringValues,osu.Game.Scoring.ScoringValues)"/>.
/// <see cref="computeScore(osu.Game.Rulesets.Scoring.ScoringMode,ScoringValues,ScoringValues)"/>.
/// </para>
/// </remarks>
/// <param name="scoreInfo">The score to extract scoring values from.</param>
/// <param name="current">The "current" scoring values, representing the hit statistics as they appear.</param>
/// <param name="maximum">The "maximum" scoring values, representing the hit statistics as if the maximum hit result was attained each time.</param>
[Pure]
internal void ExtractScoringValues(ScoreInfo scoreInfo, out ScoringValues current, out ScoringValues maximum)
private void extractScoringValues(ScoreInfo scoreInfo, out ScoringValues current, out ScoringValues maximum)
{
extractScoringValues(scoreInfo.Statistics, out current, out maximum);
current.MaxCombo = scoreInfo.MaxCombo;
@@ -490,31 +486,6 @@ namespace osu.Game.Rulesets.Scoring
extractScoringValues(scoreInfo.MaximumStatistics, out _, out maximum);
}
/// <summary>
/// Applies a best-effort extraction of hit statistics into <see cref="ScoringValues"/>.
/// </summary>
/// <remarks>
/// This method is useful in a variety of situations, with a few drawbacks that need to be considered:
/// <list type="bullet">
/// <item>The maximum <see cref="ScoringValues.BonusScore"/> will always be 0.</item>
/// <item>The current and maximum <see cref="ScoringValues.CountBasicHitObjects"/> will always be the same value.</item>
/// </list>
/// Consumers are expected to more accurately fill in the above values through external means.
/// <para>
/// <b>Ensure</b> to fill in the maximum <see cref="ScoringValues.CountBasicHitObjects"/> for use in
/// <see cref="ComputeScore(osu.Game.Rulesets.Scoring.ScoringMode,osu.Game.Scoring.ScoringValues,osu.Game.Scoring.ScoringValues)"/>.
/// </para>
/// </remarks>
/// <param name="header">The replay frame header to extract scoring values from.</param>
/// <param name="current">The "current" scoring values, representing the hit statistics as they appear.</param>
/// <param name="maximum">The "maximum" scoring values, representing the hit statistics as if the maximum hit result was attained each time.</param>
[Pure]
internal void ExtractScoringValues(FrameHeader header, out ScoringValues current, out ScoringValues maximum)
{
extractScoringValues(header.Statistics, out current, out maximum);
current.MaxCombo = header.MaxCombo;
}
/// <summary>
/// Applies a best-effort extraction of hit statistics into <see cref="ScoringValues"/>.
/// </summary>
@@ -589,6 +560,32 @@ namespace osu.Game.Rulesets.Scoring
base.Dispose(isDisposing);
hitEvents.Clear();
}
/// <summary>
/// Stores the required scoring data that fulfils the minimum requirements for a <see cref="ScoreProcessor"/> to calculate score.
/// </summary>
private struct ScoringValues
{
/// <summary>
/// The sum of all "basic" <see cref="HitObject"/> scoring values. See: <see cref="HitResultExtensions.IsBasic"/> and <see cref="Judgement.ToNumericResult"/>.
/// </summary>
public long BaseScore;
/// <summary>
/// The sum of all "bonus" <see cref="HitObject"/> scoring values. See: <see cref="HitResultExtensions.IsBonus"/> and <see cref="Judgement.ToNumericResult"/>.
/// </summary>
public long BonusScore;
/// <summary>
/// The highest achieved combo.
/// </summary>
public int MaxCombo;
/// <summary>
/// The count of "basic" <see cref="HitObject"/>s. See: <see cref="HitResultExtensions.IsBasic"/>.
/// </summary>
public int CountBasicHitObjects;
}
}
public enum ScoringMode
@@ -0,0 +1,36 @@
// 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.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
namespace osu.Game.Rulesets.UI
{
public partial class DrawableRulesetDependenciesProvidingContainer : Container
{
private readonly Ruleset ruleset;
private DrawableRulesetDependencies rulesetDependencies = null!;
public DrawableRulesetDependenciesProvidingContainer(Ruleset ruleset)
{
this.ruleset = ruleset;
RelativeSizeAxes = Axes.Both;
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
return rulesetDependencies = new DrawableRulesetDependencies(ruleset, base.CreateChildDependencies(parent));
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (rulesetDependencies.IsNotNull())
rulesetDependencies.Dispose();
}
}
}
+6 -5
View File
@@ -93,7 +93,8 @@ namespace osu.Game.Rulesets.UI
public readonly BindableBool DisplayJudgements = new BindableBool(true);
[Resolved(CanBeNull = true)]
private IReadOnlyList<Mod> mods { get; set; }
[CanBeNull]
protected IReadOnlyList<Mod> Mods { get; private set; }
private readonly HitObjectEntryManager entryManager = new HitObjectEntryManager();
@@ -243,9 +244,9 @@ namespace osu.Game.Rulesets.UI
{
base.Update();
if (!IsNested && mods != null)
if (!IsNested && Mods != null)
{
foreach (var mod in mods)
foreach (var mod in Mods)
{
if (mod is IUpdatableByPlayfield updatable)
updatable.Update(this);
@@ -374,9 +375,9 @@ namespace osu.Game.Rulesets.UI
// If this is the first time this DHO is being used, then apply the DHO mods.
// This is done before Apply() so that the state is updated once when the hitobject is applied.
if (mods != null)
if (Mods != null)
{
foreach (var m in mods.OfType<IApplicableToDrawableHitObject>())
foreach (var m in Mods.OfType<IApplicableToDrawableHitObject>())
m.ApplyToDrawableHitObject(dho);
}
}
@@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
break;
}
double lastObjectTime = Objects.LastOrDefault()?.GetEndTime() ?? double.MaxValue;
double lastObjectTime = Beatmap.HitObjects.Any() ? Beatmap.GetLastObjectTime() : double.MaxValue;
double baseBeatLength = TimingControlPoint.DEFAULT_BEAT_LENGTH;
if (RelativeScaleBeatLengths)

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