1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 22:27:25 +08:00

Merge branch 'master' into footer_V2_implementation

This commit is contained in:
MK56 2022-12-19 13:19:23 +01:00 committed by GitHub
commit 878e2f24c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
145 changed files with 1770 additions and 594 deletions

View File

@ -58,7 +58,8 @@ body:
The default places to find the logs on desktop platforms are as follows:
- `%AppData%/osu/logs` *on Windows*
- `~/.local/share/osu/logs` *on Linux & macOS*
- `~/.local/share/osu/logs` *on Linux*
- `~/Library/Application Support/osu/logs` *on macOS*
If you have selected a custom location for the game files, you can find the `logs` folder there.

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

View File

@ -23,7 +23,8 @@ Issues, bug reports and feature suggestions are welcomed, though please keep in
* the in-game logs, which are located at:
* `%AppData%/osu/logs` (on Windows),
* `~/.local/share/osu/logs` (on Linux and macOS),
* `~/.local/share/osu/logs` (on Linux),
* `~/Library/Application Support/osu/logs` (on macOS),
* `Android/data/sh.ppy.osulazer/files/logs` (on Android),
* on iOS they can be obtained by connecting your device to your desktop and [copying the `logs` directory from the app's own document storage using iTunes](https://support.apple.com/en-us/HT201301#copy-to-computer),
* your system specifications (including the operating system and platform you are playing on),

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.1130.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
osu.Android/Linker.xml Normal file
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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
var osuObject = (OsuHitObject)hitObject;
OsuHitObjectGenerationUtils.ReflectVertically(osuObject);
OsuHitObjectGenerationUtils.ReflectVerticallyAlongPlayfield(osuObject);
}
}
}

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.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;
}
}
}

View File

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

View File

@ -21,5 +21,6 @@ namespace osu.Game.Rulesets.Taiko
TaikoExplosionKiai,
Scroller,
Mascot,
KiaiGlow
}
}

View File

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

View File

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

View File

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

View File

@ -564,7 +564,7 @@ namespace osu.Game.Tests.Database
var imported = await importer.Import(
progressNotification,
new ImportTask(zipStream, string.Empty)
new[] { new ImportTask(zipStream, string.Empty) }
);
realm.Run(r => r.Refresh());
@ -1052,7 +1052,7 @@ namespace osu.Game.Tests.Database
{
string? temp = path ?? TestResources.GetTestBeatmapForImport(virtualTrack);
var importedSet = await importer.Import(new ImportTask(temp), batchImport);
var importedSet = await importer.Import(new ImportTask(temp), new ImportParameters { Batch = batchImport });
Assert.NotNull(importedSet);
Debug.Assert(importedSet != null);

View File

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

View File

@ -226,12 +226,12 @@ namespace osu.Game.Tests.Online
this.testBeatmapManager = testBeatmapManager;
}
public override Live<BeatmapSetInfo> ImportModel(BeatmapSetInfo item, ArchiveReader archive = null, bool batchImport = false, CancellationToken cancellationToken = default)
public override Live<BeatmapSetInfo> ImportModel(BeatmapSetInfo item, ArchiveReader archive = null, ImportParameters parameters = default, CancellationToken cancellationToken = default)
{
if (!testBeatmapManager.AllowImport.Wait(TimeSpan.FromSeconds(10), cancellationToken))
throw new TimeoutException("Timeout waiting for import to be allowed.");
return (testBeatmapManager.CurrentImport = base.ImportModel(item, archive, batchImport, cancellationToken));
return (testBeatmapManager.CurrentImport = base.ImportModel(item, archive, parameters, cancellationToken));
}
}
}

View File

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

View File

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

View File

@ -360,7 +360,7 @@ namespace osu.Game.Tests.Skins.IO
private async Task<Live<SkinInfo>> loadSkinIntoOsu(OsuGameBase osu, ImportTask import, bool batchImport = false)
{
var skinManager = osu.Dependencies.Get<SkinManager>();
return await skinManager.Import(import, batchImport);
return await skinManager.Import(import, new ImportParameters { Batch = batchImport });
}
}
}

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

View File

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

View File

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

View File

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

View File

@ -19,6 +19,7 @@ using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Leaderboards;
using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapListing;
using osu.Game.Overlays.Mods;
using osu.Game.Overlays.Toolbar;
using osu.Game.Rulesets.Mods;
@ -515,6 +516,28 @@ namespace osu.Game.Tests.Visual.Navigation
AddWaitStep("wait two frames", 2);
}
[Test]
public void TestFeaturedArtistDisclaimerDialog()
{
BeatmapListingOverlay getBeatmapListingOverlay() => Game.ChildrenOfType<BeatmapListingOverlay>().FirstOrDefault();
AddStep("Wait for notifications to load", () => Game.SearchBeatmapSet(string.Empty));
AddUntilStep("wait for dialog overlay", () => Game.ChildrenOfType<DialogOverlay>().SingleOrDefault() != null);
AddUntilStep("Wait for beatmap overlay to load", () => getBeatmapListingOverlay()?.State.Value == Visibility.Visible);
AddAssert("featured artist filter is on", () => getBeatmapListingOverlay().ChildrenOfType<BeatmapSearchGeneralFilterRow>().First().Current.Contains(SearchGeneral.FeaturedArtists));
AddStep("toggle featured artist filter",
() => getBeatmapListingOverlay().ChildrenOfType<FilterTabItem<SearchGeneral>>().First(i => i.Value == SearchGeneral.FeaturedArtists).TriggerClick());
AddAssert("disclaimer dialog is shown", () => Game.ChildrenOfType<DialogOverlay>().Single().CurrentDialog != null);
AddAssert("featured artist filter is still on", () => getBeatmapListingOverlay().ChildrenOfType<BeatmapSearchGeneralFilterRow>().First().Current.Contains(SearchGeneral.FeaturedArtists));
AddStep("confirm", () => InputManager.Key(Key.Enter));
AddAssert("dialog dismissed", () => Game.ChildrenOfType<DialogOverlay>().Single().CurrentDialog == null);
AddUntilStep("featured artist filter is off", () => !getBeatmapListingOverlay().ChildrenOfType<BeatmapSearchGeneralFilterRow>().First().Current.Contains(SearchGeneral.FeaturedArtists));
}
[Test]
public void TestMainOverlaysClosesNotificationOverlay()
{

View File

@ -80,6 +80,15 @@ namespace osu.Game.Tests.Visual.Online
AddStep("reset size", () => localConfig.SetValue(OsuSetting.BeatmapListingCardSize, BeatmapCardSize.Normal));
}
[Test]
public void TestFeaturedArtistFilter()
{
AddAssert("is visible", () => overlay.State.Value == Visibility.Visible);
AddAssert("featured artist filter is on", () => overlay.ChildrenOfType<BeatmapSearchGeneralFilterRow>().First().Current.Contains(SearchGeneral.FeaturedArtists));
AddStep("toggle featured artist filter", () => overlay.ChildrenOfType<FilterTabItem<SearchGeneral>>().First(i => i.Value == SearchGeneral.FeaturedArtists).TriggerClick());
AddAssert("featured artist filter is off", () => !overlay.ChildrenOfType<BeatmapSearchGeneralFilterRow>().First().Current.Contains(SearchGeneral.FeaturedArtists));
}
[Test]
public void TestHideViaBack()
{

View File

@ -77,14 +77,14 @@ namespace osu.Game.Tests.Visual.Online
{
var comments = this.ChildrenOfType<DrawableComment>();
var ourComment = comments.SingleOrDefault(x => x.Comment.Id == 1);
return ourComment != null && ourComment.ChildrenOfType<OsuSpriteText>().Any(x => x.Text == "Delete");
return ourComment != null && ourComment.ChildrenOfType<OsuSpriteText>().Any(x => x.Text == "delete");
});
AddAssert("Second doesn't", () =>
{
var comments = this.ChildrenOfType<DrawableComment>();
var ourComment = comments.Single(x => x.Comment.Id == 2);
return ourComment.ChildrenOfType<OsuSpriteText>().All(x => x.Text != "Delete");
return ourComment.ChildrenOfType<OsuSpriteText>().All(x => x.Text != "delete");
});
}
@ -102,7 +102,7 @@ namespace osu.Game.Tests.Visual.Online
});
AddStep("It has delete button", () =>
{
var btn = ourComment.ChildrenOfType<OsuSpriteText>().Single(x => x.Text == "Delete");
var btn = ourComment.ChildrenOfType<OsuSpriteText>().Single(x => x.Text == "delete");
InputManager.MoveMouseTo(btn);
});
AddStep("Click delete button", () =>
@ -175,7 +175,7 @@ namespace osu.Game.Tests.Visual.Online
});
AddStep("It has delete button", () =>
{
var btn = ourComment.ChildrenOfType<OsuSpriteText>().Single(x => x.Text == "Delete");
var btn = ourComment.ChildrenOfType<OsuSpriteText>().Single(x => x.Text == "delete");
InputManager.MoveMouseTo(btn);
});
AddStep("Click delete button", () =>
@ -245,7 +245,7 @@ namespace osu.Game.Tests.Visual.Online
});
AddStep("Click the button", () =>
{
var btn = targetComment.ChildrenOfType<OsuSpriteText>().Single(x => x.Text == "Report");
var btn = targetComment.ChildrenOfType<OsuSpriteText>().Single(x => x.Text == "report");
InputManager.MoveMouseTo(btn);
InputManager.Click(MouseButton.Left);
});

View File

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

View File

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

View File

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

View File

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

View File

@ -109,6 +109,8 @@ namespace osu.Game.Audio
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
Stop();
Track?.Dispose();
}
}

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.

View File

@ -44,7 +44,7 @@ namespace osu.Game.Beatmaps
public override async Task<Live<BeatmapSetInfo>?> ImportAsUpdate(ProgressNotification notification, ImportTask importTask, BeatmapSetInfo original)
{
var imported = await Import(notification, importTask);
var imported = await Import(notification, new[] { importTask });
if (!imported.Any())
return null;
@ -203,10 +203,10 @@ namespace osu.Game.Beatmaps
}
}
protected override void PostImport(BeatmapSetInfo model, Realm realm, bool batchImport)
protected override void PostImport(BeatmapSetInfo model, Realm realm, ImportParameters parameters)
{
base.PostImport(model, realm, batchImport);
ProcessBeatmap?.Invoke((model, batchImport));
base.PostImport(model, realm, parameters);
ProcessBeatmap?.Invoke((model, parameters.Batch));
}
private void validateOnlineIds(BeatmapSetInfo beatmapSet, Realm realm)

View File

@ -456,15 +456,15 @@ namespace osu.Game.Beatmaps
public Task Import(params string[] paths) => beatmapImporter.Import(paths);
public Task Import(params ImportTask[] tasks) => beatmapImporter.Import(tasks);
public Task Import(ImportTask[] tasks, ImportParameters parameters = default) => beatmapImporter.Import(tasks, parameters);
public Task<IEnumerable<Live<BeatmapSetInfo>>> Import(ProgressNotification notification, params ImportTask[] tasks) => beatmapImporter.Import(notification, tasks);
public Task<IEnumerable<Live<BeatmapSetInfo>>> Import(ProgressNotification notification, ImportTask[] tasks, ImportParameters parameters = default) => beatmapImporter.Import(notification, tasks, parameters);
public Task<Live<BeatmapSetInfo>?> Import(ImportTask task, bool batchImport = false, CancellationToken cancellationToken = default) =>
beatmapImporter.Import(task, batchImport, cancellationToken);
public Task<Live<BeatmapSetInfo>?> Import(ImportTask task, ImportParameters parameters = default, CancellationToken cancellationToken = default) =>
beatmapImporter.Import(task, parameters, cancellationToken);
public Live<BeatmapSetInfo>? Import(BeatmapSetInfo item, ArchiveReader? archive = null, CancellationToken cancellationToken = default) =>
beatmapImporter.ImportModel(item, archive, false, cancellationToken);
beatmapImporter.ImportModel(item, archive, default, cancellationToken);
public IEnumerable<string> HandledExtensions => beatmapImporter.HandledExtensions;

View File

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

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

View File

@ -141,6 +141,9 @@ namespace osu.Game.Beatmaps
try
{
string fileStorePath = BeatmapSetInfo.GetPathForFile(BeatmapInfo.Path);
// TODO: check validity of file
var stream = GetStream(fileStorePath);
if (stream == null)

View File

@ -19,6 +19,7 @@ namespace osu.Game.Configuration
SetDefault(Static.LoginOverlayDisplayed, false);
SetDefault(Static.MutedAudioNotificationShownOnce, false);
SetDefault(Static.LowBatteryNotificationShownOnce, false);
SetDefault(Static.FeaturedArtistDisclaimerShownOnce, false);
SetDefault(Static.LastHoverSoundPlaybackTime, (double?)null);
SetDefault<APISeasonalBackgrounds>(Static.SeasonalBackgrounds, null);
}
@ -42,6 +43,7 @@ namespace osu.Game.Configuration
LoginOverlayDisplayed,
MutedAudioNotificationShownOnce,
LowBatteryNotificationShownOnce,
FeaturedArtistDisclaimerShownOnce,
/// <summary>
/// Info about seasonal backgrounds available fetched from API - see <see cref="APISeasonalBackgrounds"/>.
@ -53,6 +55,6 @@ namespace osu.Game.Configuration
/// The last playback time in milliseconds of a hover sample (from <see cref="HoverSounds"/>).
/// Used to debounce hover sounds game-wide to avoid volume saturation, especially in scrolling views with many UI controls like <see cref="SettingsOverlay"/>.
/// </summary>
LastHoverSoundPlaybackTime
LastHoverSoundPlaybackTime,
}
}

View File

@ -31,7 +31,8 @@ namespace osu.Game.Database
/// This will post notifications tracking progress.
/// </remarks>
/// <param name="tasks">The import tasks from which the files should be imported.</param>
Task Import(params ImportTask[] tasks);
/// <param name="parameters">Parameters to further configure the import process.</param>
Task Import(ImportTask[] tasks, ImportParameters parameters = default);
/// <summary>
/// An array of accepted file extensions (in the standard format of ".abc").

View File

@ -20,8 +20,9 @@ namespace osu.Game.Database
/// </summary>
/// <param name="notification">The notification to update.</param>
/// <param name="tasks">The import tasks.</param>
/// <param name="parameters">Parameters to further configure the import process.</param>
/// <returns>The imported models.</returns>
Task<IEnumerable<Live<TModel>>> Import(ProgressNotification notification, params ImportTask[] tasks);
Task<IEnumerable<Live<TModel>>> Import(ProgressNotification notification, ImportTask[] tasks, ImportParameters parameters = default);
/// <summary>
/// Process a single import as an update for an existing model.

View File

@ -0,0 +1,25 @@
// 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.
namespace osu.Game.Database
{
public struct ImportParameters
{
/// <summary>
/// Whether this import is part of a larger batch.
/// </summary>
/// <remarks>
/// May skip intensive pre-import checks in favour of faster processing.
///
/// More specifically, imports will be skipped before they begin, given an existing model matches on hash and filenames. Should generally only be used for large batch imports, as it may defy user expectations when updating an existing model.
///
/// Will also change scheduling behaviour to run at a lower priority.
/// </remarks>
public bool Batch { get; set; }
/// <summary>
/// Whether this import should use hard links rather than file copy operations if available.
/// </summary>
public bool PreferHardLinks { get; set; }
}
}

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

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,20 @@ 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 bool CheckHardLinkAvailability()
{
var stableStorage = GetCurrentStableStorage();
if (stableStorage == null || gameHost is not DesktopGameHost desktopGameHost)
return false;
string testExistingPath = stableStorage.GetFullPath(string.Empty);
string testDestinationPath = desktopGameHost.Storage.GetFullPath(string.Empty);
return HardLinkHelper.CheckAvailability(testDestinationPath, testExistingPath);
}
public virtual async Task<int> GetImportCount(StableContent content, CancellationToken cancellationToken)
{

View File

@ -57,7 +57,12 @@ namespace osu.Game.Database
return Task.CompletedTask;
}
return Task.Run(async () => await Importer.Import(GetStableImportPaths(storage).ToArray()).ConfigureAwait(false));
return Task.Run(async () =>
{
var tasks = GetStableImportPaths(storage).Select(p => new ImportTask(p)).ToArray();
await Importer.Import(tasks, new ImportParameters { Batch = true, PreferHardLinks = true }).ConfigureAwait(false);
});
}
/// <summary>

View File

@ -73,7 +73,7 @@ namespace osu.Game.Database
if (originalModel != null)
importSuccessful = (await importer.ImportAsUpdate(notification, new ImportTask(filename), originalModel)) != null;
else
importSuccessful = (await importer.Import(notification, new ImportTask(filename))).Any();
importSuccessful = (await importer.Import(notification, new[] { new ImportTask(filename) })).Any();
// for now a failed import will be marked as a failed download for simplicity.
if (!importSuccessful)

View File

@ -81,16 +81,16 @@ namespace osu.Game.Database
public Task Import(params string[] paths) => Import(paths.Select(p => new ImportTask(p)).ToArray());
public Task Import(params ImportTask[] tasks)
public Task Import(ImportTask[] tasks, ImportParameters parameters = default)
{
var notification = new ProgressNotification { State = ProgressNotificationState.Active };
PostNotification?.Invoke(notification);
return Import(notification, tasks);
return Import(notification, tasks, parameters);
}
public async Task<IEnumerable<Live<TModel>>> Import(ProgressNotification notification, params ImportTask[] tasks)
public async Task<IEnumerable<Live<TModel>>> Import(ProgressNotification notification, ImportTask[] tasks, ImportParameters parameters = default)
{
if (tasks.Length == 0)
{
@ -106,7 +106,7 @@ namespace osu.Game.Database
var imported = new List<Live<TModel>>();
bool isBatchImport = tasks.Length >= minimum_items_considered_batch_import;
parameters.Batch |= tasks.Length >= minimum_items_considered_batch_import;
await Task.WhenAll(tasks.Select(async task =>
{
@ -115,7 +115,7 @@ namespace osu.Game.Database
try
{
var model = await Import(task, isBatchImport, notification.CancellationToken).ConfigureAwait(false);
var model = await Import(task, parameters, notification.CancellationToken).ConfigureAwait(false);
lock (imported)
{
@ -176,16 +176,16 @@ namespace osu.Game.Database
/// Note that this bypasses the UI flow and should only be used for special cases or testing.
/// </summary>
/// <param name="task">The <see cref="ImportTask"/> containing data about the <typeparamref name="TModel"/> to import.</param>
/// <param name="batchImport">Whether this import is part of a larger batch.</param>
/// <param name="parameters">Parameters to further configure the import process.</param>
/// <param name="cancellationToken">An optional cancellation token.</param>
/// <returns>The imported model, if successful.</returns>
public async Task<Live<TModel>?> Import(ImportTask task, bool batchImport = false, CancellationToken cancellationToken = default)
public async Task<Live<TModel>?> Import(ImportTask task, ImportParameters parameters = default, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
Live<TModel>? import;
using (ArchiveReader reader = task.GetReader())
import = await importFromArchive(reader, batchImport, cancellationToken).ConfigureAwait(false);
import = await importFromArchive(reader, parameters, cancellationToken).ConfigureAwait(false);
// We may or may not want to delete the file depending on where it is stored.
// e.g. reconstructing/repairing database with items from default storage.
@ -211,9 +211,9 @@ namespace osu.Game.Database
/// This method also handled queueing the import task on a relevant import thread pool.
/// </remarks>
/// <param name="archive">The archive to be imported.</param>
/// <param name="batchImport">Whether this import is part of a larger batch.</param>
/// <param name="parameters">Parameters to further configure the import process.</param>
/// <param name="cancellationToken">An optional cancellation token.</param>
private async Task<Live<TModel>?> importFromArchive(ArchiveReader archive, bool batchImport = false, CancellationToken cancellationToken = default)
private async Task<Live<TModel>?> importFromArchive(ArchiveReader archive, ImportParameters parameters = default, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
@ -236,10 +236,10 @@ namespace osu.Game.Database
return null;
}
var scheduledImport = Task.Factory.StartNew(() => ImportModel(model, archive, batchImport, cancellationToken),
var scheduledImport = Task.Factory.StartNew(() => ImportModel(model, archive, parameters, cancellationToken),
cancellationToken,
TaskCreationOptions.HideScheduler,
batchImport ? import_scheduler_batch : import_scheduler);
parameters.Batch ? import_scheduler_batch : import_scheduler);
return await scheduledImport.ConfigureAwait(false);
}
@ -249,15 +249,15 @@ namespace osu.Game.Database
/// </summary>
/// <param name="item">The model to be imported.</param>
/// <param name="archive">An optional archive to use for model population.</param>
/// <param name="batchImport">If <c>true</c>, imports will be skipped before they begin, given an existing model matches on hash and filenames. Should generally only be used for large batch imports, as it may defy user expectations when updating an existing model.</param>
/// <param name="parameters">Parameters to further configure the import process.</param>
/// <param name="cancellationToken">An optional cancellation token.</param>
public virtual Live<TModel>? ImportModel(TModel item, ArchiveReader? archive = null, bool batchImport = false, CancellationToken cancellationToken = default) => Realm.Run(realm =>
public virtual Live<TModel>? ImportModel(TModel item, ArchiveReader? archive = null, ImportParameters parameters = default, CancellationToken cancellationToken = default) => Realm.Run(realm =>
{
cancellationToken.ThrowIfCancellationRequested();
TModel? existing;
if (batchImport && archive != null)
if (parameters.Batch && archive != null)
{
// this is a fast bail condition to improve large import performance.
item.Hash = computeHashFast(archive);
@ -303,7 +303,7 @@ namespace osu.Game.Database
foreach (var filenames in getShortenedFilenames(archive))
{
using (Stream s = archive.GetStream(filenames.original))
files.Add(new RealmNamedFileUsage(Files.Add(s, realm, false), filenames.shortened));
files.Add(new RealmNamedFileUsage(Files.Add(s, realm, false, parameters.PreferHardLinks), filenames.shortened));
}
}
@ -358,7 +358,7 @@ namespace osu.Game.Database
// import to store
realm.Add(item);
PostImport(item, realm, batchImport);
PostImport(item, realm, parameters);
transaction.Commit();
}
@ -493,8 +493,8 @@ namespace osu.Game.Database
/// </summary>
/// <param name="model">The model prepared for import.</param>
/// <param name="realm">The current realm context.</param>
/// <param name="batchImport">Whether the import was part of a batch.</param>
protected virtual void PostImport(TModel model, Realm realm, bool batchImport)
/// <param name="parameters">Parameters to further configure the import process.</param>
protected virtual void PostImport(TModel model, Realm realm, ImportParameters parameters)
{
}

View File

@ -4,12 +4,14 @@
using System;
using System.IO;
using System.Linq;
using osu.Framework;
using osu.Framework.Extensions;
using osu.Framework.IO.Stores;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Extensions;
using osu.Game.IO;
using osu.Game.Models;
using Realms;
@ -41,7 +43,8 @@ namespace osu.Game.Database
/// <param name="data">The file data stream.</param>
/// <param name="realm">The realm instance to add to. Should already be in a transaction.</param>
/// <param name="addToRealm">Whether the <see cref="RealmFile"/> should immediately be added to the underlying realm. If <c>false</c> is provided here, the instance must be manually added.</param>
public RealmFile Add(Stream data, Realm realm, bool addToRealm = true)
/// <param name="preferHardLinks">Whether this import should use hard links rather than file copy operations if available.</param>
public RealmFile Add(Stream data, Realm realm, bool addToRealm = true, bool preferHardLinks = false)
{
string hash = data.ComputeSHA2Hash();
@ -50,7 +53,7 @@ namespace osu.Game.Database
var file = existing ?? new RealmFile { Hash = hash };
if (!checkFileExistsAndMatchesHash(file))
copyToStore(file, data);
copyToStore(file, data, preferHardLinks);
if (addToRealm && !file.IsManaged)
realm.Add(file);
@ -58,8 +61,15 @@ namespace osu.Game.Database
return file;
}
private void copyToStore(RealmFile file, Stream data)
private void copyToStore(RealmFile file, Stream data, bool preferHardLinks)
{
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows && data is FileStream fs && preferHardLinks)
{
// attempt to do a fast hard link rather than copy.
if (HardLinkHelper.CreateHardLink(Storage.GetFullPath(file.GetStoragePath(), true), fs.Name, IntPtr.Zero))
return;
}
data.Seek(0, SeekOrigin.Begin);
using (var output = Storage.CreateFileSafely(file.GetStoragePath()))

View File

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

View File

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

View File

@ -234,7 +234,7 @@ namespace osu.Game.Graphics.Cursor
SampleChannel channel = tapSample.GetChannel();
// Scale to [-0.75, 0.75] so that the sample isn't fully panned left or right (sounds weird)
channel.Balance.Value = ((activeCursor.X / DrawWidth) * 2 - 1) * 0.75;
channel.Balance.Value = ((activeCursor.X / DrawWidth) * 2 - 1) * OsuGameBase.SFX_STEREO_STRENGTH;
channel.Frequency.Value = baseFrequency - (random_range / 2f) + RNG.NextDouble(random_range);
channel.Volume.Value = baseFrequency;

View File

@ -9,6 +9,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites;
using osuTK;
@ -69,8 +70,8 @@ namespace osu.Game.Graphics
{
DateTimeOffset localDate = date.ToLocalTime();
dateText.Text = $"{localDate:d MMMM yyyy} ";
timeText.Text = $"{localDate:HH:mm:ss \"UTC\"z}";
dateText.Text = LocalisableString.Interpolate($"{localDate:d MMMM yyyy} ");
timeText.Text = LocalisableString.Interpolate($"{localDate:HH:mm:ss \"UTC\"z}");
}
public void Move(Vector2 pos) => Position = pos;

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]

View File

@ -197,7 +197,7 @@ namespace osu.Game.Graphics.UserInterface
}, true);
}
[BackgroundDependencyLoader]
[BackgroundDependencyLoader(true)]
private void load(OverlayColourProvider? colourProvider)
{
if (colourProvider == null) return;

View File

@ -0,0 +1,108 @@
// 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.IO;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using Microsoft.Win32.SafeHandles;
using osu.Framework;
namespace osu.Game.IO
{
internal static class HardLinkHelper
{
public static bool CheckAvailability(string testDestinationPath, string testSourcePath)
{
// We can support other operating systems quite easily in the future.
// Let's handle the most common one for now, though.
if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows)
return false;
const string test_filename = "_hard_link_test";
testDestinationPath = Path.Combine(testDestinationPath, test_filename);
testSourcePath = Path.Combine(testSourcePath, test_filename);
cleanupFiles();
try
{
File.WriteAllText(testSourcePath, string.Empty);
// Test availability by creating an arbitrary hard link between the source and destination paths.
return CreateHardLink(testDestinationPath, testSourcePath, IntPtr.Zero);
}
catch
{
return false;
}
finally
{
cleanupFiles();
}
void cleanupFiles()
{
try
{
File.Delete(testDestinationPath);
File.Delete(testSourcePath);
}
catch
{
}
}
}
// For future use (to detect if a file is a hard link with other references existing on disk).
public static int GetFileLinkCount(string filePath)
{
int result = 0;
SafeFileHandle handle = CreateFile(filePath, FileAccess.Read, FileShare.Read, IntPtr.Zero, FileMode.Open, FileAttributes.Archive, IntPtr.Zero);
ByHandleFileInformation fileInfo;
if (GetFileInformationByHandle(handle, out fileInfo))
result = (int)fileInfo.NumberOfLinks;
CloseHandle(handle);
return result;
}
[DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern bool CreateHardLink(string lpFileName, string lpExistingFileName, IntPtr lpSecurityAttributes);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
private static extern SafeFileHandle CreateFile(
string lpFileName,
[MarshalAs(UnmanagedType.U4)] FileAccess dwDesiredAccess,
[MarshalAs(UnmanagedType.U4)] FileShare dwShareMode,
IntPtr lpSecurityAttributes,
[MarshalAs(UnmanagedType.U4)] FileMode dwCreationDisposition,
[MarshalAs(UnmanagedType.U4)] FileAttributes dwFlagsAndAttributes,
IntPtr hTemplateFile);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool GetFileInformationByHandle(SafeFileHandle handle, out ByHandleFileInformation lpFileInformation);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool CloseHandle(SafeHandle hObject);
[StructLayout(LayoutKind.Sequential)]
private struct ByHandleFileInformation
{
public readonly uint FileAttributes;
public readonly FILETIME CreationTime;
public readonly FILETIME LastAccessTime;
public readonly FILETIME LastWriteTime;
public readonly uint VolumeSerialNumber;
public readonly uint FileSizeHigh;
public readonly uint FileSizeLow;
public readonly uint NumberOfLinks;
public readonly uint FileIndexHigh;
public readonly uint FileIndexLow;
}
}
}

View File

@ -0,0 +1,33 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Localisation
{
public static class BeatmapOverlayStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.BeatmapOverlayStrings";
/// <summary>
/// "User content disclaimer"
/// </summary>
public static LocalisableString UserContentDisclaimerHeader => new TranslatableString(getKey(@"user_content_disclaimer"), @"User content disclaimer");
/// <summary>
/// "By turning off the &quot;Featured Artist&quot; filter, all user-uploaded content will be displayed.
///
/// This includes content that may not be correctly licensed for osu! usage. Browse at your own risk."
/// </summary>
public static LocalisableString UserContentDisclaimerDescription => new TranslatableString(getKey(@"by_turning_off_the_featured"), @"By turning off the ""Featured Artist"" filter, all user-uploaded content will be displayed.
This includes content that may not be correctly licensed for osu! usage. Browse at your own risk.");
/// <summary>
/// "I understand"
/// </summary>
public static LocalisableString UserContentConfirmButtonText => new TranslatableString(getKey(@"understood"), @"I understand");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -15,10 +15,10 @@ namespace osu.Game.Localisation
public static LocalisableString Header => new TranslatableString(getKey(@"header"), @"Import");
/// <summary>
/// "If you have an installation of a previous osu! version, you can choose to migrate your existing content. Note that this will create a copy, and not affect your existing installation."
/// "If you have an installation of a previous osu! version, you can choose to migrate your existing content. Note that this will not affect your existing installation's files in any way."
/// </summary>
public static LocalisableString Description => new TranslatableString(getKey(@"description"),
@"If you have an installation of a previous osu! version, you can choose to migrate your existing content. Note that this will create a copy, and not affect your existing installation.");
@"If you have an installation of a previous osu! version, you can choose to migrate your existing content. Note that this will not affect your existing installation's files in any way.");
/// <summary>
/// "previous osu! install"

View File

@ -32,6 +32,7 @@ namespace osu.Game.Online.API.Requests
Loved,
Pending,
Guest,
Graveyard
Graveyard,
Nominated,
}
}

View File

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

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 =>
{

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;

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

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

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

View File

@ -136,9 +136,8 @@ namespace osu.Game.Online.Leaderboards
{
if (displayedScore != null)
{
timestampLabel.Text = prefer24HourTime.Value
? $"Played on {displayedScore.Date.ToLocalTime():d MMMM yyyy HH:mm}"
: $"Played on {displayedScore.Date.ToLocalTime():d MMMM yyyy h:mm tt}";
timestampLabel.Text = LocalisableString.Format("Played on {0}",
displayedScore.Date.ToLocalTime().ToLocalisableString(prefer24HourTime.Value ? @"d MMMM yyyy HH:mm" : @"d MMMM yyyy h:mm tt"));
}
}

View File

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

View File

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

View File

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

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

View File

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

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

View File

@ -616,14 +616,14 @@ namespace osu.Game
}, validScreens: validScreens);
}
public override Task Import(params ImportTask[] imports)
public override Task Import(ImportTask[] imports, ImportParameters parameters = default)
{
// encapsulate task as we don't want to begin the import process until in a ready state.
// ReSharper disable once AsyncVoidLambda
// TODO: This is bad because `new Task` doesn't have a Func<Task?> override.
// Only used for android imports and a bit of a mess. Probably needs rethinking overall.
var importTask = new Task(async () => await base.Import(imports).ConfigureAwait(false));
var importTask = new Task(async () => await base.Import(imports, parameters).ConfigureAwait(false));
waitForReady(() => this, _ => importTask.Start());

View File

@ -83,6 +83,8 @@ namespace osu.Game
public const int SAMPLE_CONCURRENCY = 6;
public const double SFX_STEREO_STRENGTH = 0.75;
/// <summary>
/// Length of debounce (in milliseconds) for commonly occuring sample playbacks that could stack.
/// </summary>

View File

@ -44,13 +44,13 @@ namespace osu.Game
}
}
public virtual async Task Import(params ImportTask[] tasks)
public virtual async Task Import(ImportTask[] tasks, ImportParameters parameters = default)
{
var tasksPerExtension = tasks.GroupBy(t => Path.GetExtension(t.Path).ToLowerInvariant());
await Task.WhenAll(tasksPerExtension.Select(taskGroup =>
{
var importer = fileImporters.FirstOrDefault(i => i.HandledExtensions.Contains(taskGroup.Key));
return importer?.Import(taskGroup.ToArray()) ?? Task.CompletedTask;
return importer?.Import(taskGroup.ToArray(), parameters) ?? Task.CompletedTask;
})).ConfigureAwait(false);
}

View File

@ -146,6 +146,7 @@ namespace osu.Game.Overlays.BeatmapListing
}
});
generalFilter.Current.Add(SearchGeneral.FeaturedArtists);
categoryFilter.Current.Value = SearchCategory.Leaderboard;
}

View File

@ -3,10 +3,18 @@
#nullable disable
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Localisation;
using osu.Game.Overlays.Dialog;
using osu.Game.Resources.Localisation.Web;
using osuTK.Graphics;
using CommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings;
namespace osu.Game.Overlays.BeatmapListing
{
@ -32,6 +40,8 @@ namespace osu.Game.Overlays.BeatmapListing
private partial class FeaturedArtistsTabItem : MultipleSelectionFilterTabItem
{
private Bindable<bool> disclaimerShown;
public FeaturedArtistsTabItem()
: base(SearchGeneral.FeaturedArtists)
{
@ -40,7 +50,60 @@ namespace osu.Game.Overlays.BeatmapListing
[Resolved]
private OsuColour colours { get; set; }
[Resolved]
private SessionStatics sessionStatics { get; set; }
[Resolved(canBeNull: true)]
private IDialogOverlay dialogOverlay { get; set; }
protected override Color4 GetStateColour() => colours.Orange1;
protected override void LoadComplete()
{
base.LoadComplete();
disclaimerShown = sessionStatics.GetBindable<bool>(Static.FeaturedArtistDisclaimerShownOnce);
}
protected override bool OnClick(ClickEvent e)
{
if (!disclaimerShown.Value && dialogOverlay != null)
{
dialogOverlay.Push(new FeaturedArtistConfirmDialog(() =>
{
disclaimerShown.Value = true;
base.OnClick(e);
}));
return true;
}
return base.OnClick(e);
}
}
}
internal partial class FeaturedArtistConfirmDialog : PopupDialog
{
public FeaturedArtistConfirmDialog(Action confirm)
{
HeaderText = BeatmapOverlayStrings.UserContentDisclaimerHeader;
BodyText = BeatmapOverlayStrings.UserContentDisclaimerDescription;
Icon = FontAwesome.Solid.ExclamationTriangle;
Buttons = new PopupDialogButton[]
{
new PopupDialogDangerousButton
{
Text = BeatmapOverlayStrings.UserContentConfirmButtonText,
Action = confirm
},
new PopupDialogCancelButton
{
Text = CommonStrings.ButtonsCancel,
},
};
}
}
}

View File

@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
@ -18,6 +19,7 @@ using osuTK;
namespace osu.Game.Overlays.BeatmapListing
{
public partial class BeatmapSearchMultipleSelectionFilterRow<T> : BeatmapSearchFilterRow<List<T>>
where T : Enum
{
public new readonly BindableList<T> Current = new BindableList<T>();
@ -31,7 +33,7 @@ namespace osu.Game.Overlays.BeatmapListing
[BackgroundDependencyLoader]
private void load()
{
Current.BindTo(filter.Current);
filter.Current.BindTo(Current);
}
protected sealed override Drawable CreateFilter() => filter = CreateMultipleSelectionFilter();
@ -64,6 +66,14 @@ namespace osu.Game.Overlays.BeatmapListing
foreach (var item in Children)
item.Active.BindValueChanged(active => toggleItem(item.Value, active.NewValue));
Current.BindCollectionChanged(currentChanged, true);
}
private void currentChanged(object sender, NotifyCollectionChangedEventArgs e)
{
foreach (var c in Children)
c.Active.Value = Current.Contains(c.Value);
}
/// <summary>
@ -79,7 +89,10 @@ namespace osu.Game.Overlays.BeatmapListing
private void toggleItem(T value, bool active)
{
if (active)
Current.Add(value);
{
if (!Current.Contains(value))
Current.Add(value);
}
else
Current.Remove(value);
}

View File

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

View File

@ -6,6 +6,7 @@
using System;
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@ -51,7 +52,7 @@ namespace osu.Game.Overlays.Changelog
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Margin = new MarginPadding { Top = 20 },
Text = build.CreatedAt.Date.ToString("dd MMMM yyyy"),
Text = build.CreatedAt.Date.ToLocalisableString("dd MMMM yyyy"),
Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 24),
});

View File

@ -4,10 +4,10 @@
#nullable disable
using System;
using System.Linq;
using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@ -104,27 +104,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.ToLocalisableString("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)

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@ -175,9 +176,7 @@ namespace osu.Game.Overlays.Chat
private void updateTimestamp()
{
drawableTimestamp.Text = prefer24HourTime.Value
? $@"{message.Timestamp.LocalDateTime:HH:mm:ss}"
: $@"{message.Timestamp.LocalDateTime:hh:mm:ss tt}";
drawableTimestamp.Text = message.Timestamp.LocalDateTime.ToLocalisableString(prefer24HourTime.Value ? @"HH:mm:ss" : @"hh:mm:ss tt");
}
}
}

View File

@ -30,7 +30,7 @@ namespace osu.Game.Overlays.Chat
public Color4 AccentColour { get; }
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
Child.ReceivePositionalInputAt(screenSpacePos);
colouredDrawable.ReceivePositionalInputAt(screenSpacePos);
public float FontSize
{
@ -87,13 +87,13 @@ namespace osu.Game.Overlays.Chat
{
AccentColour = default_colours[user.Id % default_colours.Length];
Child = colouredDrawable = drawableText;
Add(colouredDrawable = drawableText);
}
else
{
AccentColour = Color4Extensions.FromHex(user.Colour);
Child = new Container
Add(new Container
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
@ -127,7 +127,7 @@ namespace osu.Game.Overlays.Chat
}
}
}
};
});
}
}

View File

@ -3,6 +3,7 @@
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
@ -53,7 +54,7 @@ namespace osu.Game.Overlays.Comments
}
};
link.AddLink(UsersStrings.ReportButtonText, this.ShowPopover);
link.AddLink(ReportStrings.CommentButton.ToLower(), this.ShowPopover);
}
private void report(CommentReportReason reason, string comments)

View File

@ -19,6 +19,7 @@ using System;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Extensions.IEnumerableExtensions;
using System.Collections.Specialized;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Localisation;
using osu.Framework.Platform;
using osu.Game.Graphics.UserInterface;
@ -335,7 +336,7 @@ namespace osu.Game.Overlays.Comments
actionsContainer.AddArbitraryDrawable(Empty().With(d => d.Width = 10));
if (Comment.UserId.HasValue && Comment.UserId.Value == api.LocalUser.Value.Id)
actionsContainer.AddLink(CommonStrings.ButtonsDelete, deleteComment);
actionsContainer.AddLink(CommonStrings.ButtonsDelete.ToLower(), deleteComment);
else
actionsContainer.AddArbitraryDrawable(new CommentReportButton(Comment));

View File

@ -5,6 +5,7 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
@ -167,7 +168,7 @@ namespace osu.Game.Overlays.Dashboard.Home.News
Origin = Anchor.TopRight,
Font = OsuFont.GetFont(weight: FontWeight.Bold), // using Bold since there is no 800 weight alternative
Colour = colourProvider.Light1,
Text = $"{date:dd}"
Text = date.ToLocalisableString(@"dd")
},
new TextFlowContainer(f =>
{
@ -178,7 +179,7 @@ namespace osu.Game.Overlays.Dashboard.Home.News
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
AutoSizeAxes = Axes.Both,
Text = $"{date:MMM yyyy}"
Text = date.ToLocalisableString(@"MMM yyyy")
}
}
};

View File

@ -5,6 +5,7 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
@ -98,12 +99,12 @@ namespace osu.Game.Overlays.Dashboard.Home.News
Margin = new MarginPadding { Vertical = 5 }
};
textFlow.AddText($"{date:dd}", t =>
textFlow.AddText(date.ToLocalisableString(@"dd"), t =>
{
t.Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold);
});
textFlow.AddText($"{date: MMM}", t =>
textFlow.AddText(date.ToLocalisableString(@" MMM"), t =>
{
t.Font = OsuFont.GetFont(size: 14, weight: FontWeight.Regular);
});

View File

@ -198,6 +198,7 @@ namespace osu.Game.Overlays.Dialog
TextAnchor = Anchor.TopCentre,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding(5),
},
},
},

View File

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

View File

@ -7,6 +7,7 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
@ -14,12 +15,15 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Localisation;
using osu.Game.Overlays.Settings;
using osu.Game.Overlays.Settings.Sections.Maintenance;
using osu.Game.Screens.Edit.Setup;
using osuTK;
@ -39,6 +43,8 @@ namespace osu.Game.Overlays.FirstRunSetup
private StableLocatorLabelledTextBox stableLocatorTextBox = null!;
private LinkFlowContainer copyInformation = null!;
private IEnumerable<ImportCheckbox> contentCheckboxes => Content.Children.OfType<ImportCheckbox>();
[BackgroundDependencyLoader(permitNulls: true)]
@ -46,7 +52,7 @@ namespace osu.Game.Overlays.FirstRunSetup
{
Content.Children = new Drawable[]
{
new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE))
new LinkFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE))
{
Colour = OverlayColourProvider.Content1,
Text = FirstRunOverlayImportFromStableScreenStrings.Description,
@ -62,6 +68,12 @@ namespace osu.Game.Overlays.FirstRunSetup
new ImportCheckbox(CommonStrings.Scores, StableContent.Scores),
new ImportCheckbox(CommonStrings.Skins, StableContent.Skins),
new ImportCheckbox(CommonStrings.Collections, StableContent.Collections),
copyInformation = new LinkFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE))
{
Colour = OverlayColourProvider.Content1,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
},
importButton = new ProgressRoundedButton
{
Size = button_size,
@ -83,6 +95,9 @@ namespace osu.Game.Overlays.FirstRunSetup
stableLocatorTextBox.Current.BindValueChanged(_ => updateStablePath(), true);
}
[Resolved(canBeNull: true)]
private OsuGame? game { get; set; }
private void updateStablePath()
{
var storage = legacyImportManager.GetCurrentStableStorage();
@ -105,6 +120,25 @@ namespace osu.Game.Overlays.FirstRunSetup
toggleInteraction(true);
stableLocatorTextBox.Current.Value = storage.GetFullPath(string.Empty);
importButton.Enabled.Value = true;
bool available = legacyImportManager.CheckHardLinkAvailability();
Logger.Log($"Hard link support is {available}");
if (available)
{
copyInformation.Text = "Data migration will use \"hard links\". No extra disk space will be used, and you can delete either data folder at any point without affecting the other installation.";
}
else if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows)
copyInformation.Text = "Lightweight linking of files is not supported on your operating system yet, so a copy of all files will be made during import.";
else
{
copyInformation.Text =
"A second copy of all files will be made during import. To avoid this, please make sure the lazer data folder is on the same drive as your previous osu! install (and the file system is NTFS). ";
copyInformation.AddLink(GeneralSettingsStrings.ChangeFolderLocation, () =>
{
game?.PerformFromScreen(menu => menu.Push(new MigrationSelectScreen()));
});
}
}
private void runImport()
@ -235,7 +269,7 @@ namespace osu.Game.Overlays.FirstRunSetup
return Task.CompletedTask;
}
Task ICanAcceptFiles.Import(params ImportTask[] tasks) => throw new NotImplementedException();
Task ICanAcceptFiles.Import(ImportTask[] tasks, ImportParameters parameters) => throw new NotImplementedException();
protected override void Dispose(bool isDisposing)
{

View File

@ -19,6 +19,7 @@ using osu.Framework.Graphics.Sprites;
using System.Diagnostics;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Platform;
namespace osu.Game.Overlays.News.Sidebar
@ -99,7 +100,7 @@ namespace osu.Game.Overlays.News.Sidebar
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold),
Text = date.ToString("MMM yyyy")
Text = date.ToLocalisableString(@"MMM yyyy")
},
icon = new SpriteIcon
{

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