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:
commit
878e2f24c4
3
.github/ISSUE_TEMPLATE/bug-issue.yml
vendored
3
.github/ISSUE_TEMPLATE/bug-issue.yml
vendored
@ -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.
|
||||
|
||||
|
2
.github/workflows/report-nunit.yml
vendored
2
.github/workflows/report-nunit.yml
vendored
@ -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}})
|
||||
|
@ -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),
|
||||
|
@ -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
7
osu.Android/Linker.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<linker>
|
||||
<assembly fullname="mscorlib">
|
||||
<!-- see https://github.com/ppy/osu/issues/21516 -->
|
||||
<type fullname="System.Globalization.*Calendar"/>
|
||||
</assembly>
|
||||
</linker>
|
@ -4,8 +4,10 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Rulesets.Catch.Mods;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
@ -55,6 +57,21 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods
|
||||
}
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestGameCursorHidden()
|
||||
{
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new CatchModRelax(),
|
||||
Autoplay = false,
|
||||
PassCondition = () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<DrawableCatchRuleset>().Single());
|
||||
return this.ChildrenOfType<MenuCursorContainer>().Single().State.Value == Visibility.Hidden;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private bool passCondition()
|
||||
{
|
||||
var playfield = this.ChildrenOfType<CatchPlayfield>().Single();
|
||||
|
15
osu.Game.Rulesets.Catch/UI/CatchCursorContainer.cs
Normal file
15
osu.Game.Rulesets.Catch/UI/CatchCursorContainer.cs
Normal 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();
|
||||
}
|
||||
}
|
@ -10,6 +10,7 @@ using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osuTK;
|
||||
|
||||
@ -49,6 +50,8 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
this.difficulty = difficulty;
|
||||
}
|
||||
|
||||
protected override GameplayCursorContainer CreateCursor() => new CatchCursorContainer();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
|
@ -90,6 +90,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
public override bool CursorInPlacementArea => false;
|
||||
|
||||
public TestHitObjectComposer(Playfield playfield)
|
||||
: base(new ManiaRuleset())
|
||||
{
|
||||
Playfield = playfield;
|
||||
}
|
||||
|
@ -0,0 +1,91 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.Osu.Utils;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class OsuHitObjectGenerationUtilsTest
|
||||
{
|
||||
private static Slider createTestSlider()
|
||||
{
|
||||
var slider = new Slider
|
||||
{
|
||||
Position = new Vector2(128, 128),
|
||||
Path = new SliderPath
|
||||
{
|
||||
ControlPoints =
|
||||
{
|
||||
new PathControlPoint(new Vector2(), PathType.Linear),
|
||||
new PathControlPoint(new Vector2(-64, -128), PathType.Linear), // absolute position: (64, 0)
|
||||
new PathControlPoint(new Vector2(-128, 0), PathType.Linear) // absolute position: (0, 128)
|
||||
}
|
||||
},
|
||||
RepeatCount = 1
|
||||
};
|
||||
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
return slider;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestReflectSliderHorizontallyAlongPlayfield()
|
||||
{
|
||||
var slider = createTestSlider();
|
||||
|
||||
OsuHitObjectGenerationUtils.ReflectHorizontallyAlongPlayfield(slider);
|
||||
|
||||
Assert.That(slider.Position, Is.EqualTo(new Vector2(OsuPlayfield.BASE_SIZE.X - 128, 128)));
|
||||
Assert.That(slider.NestedHitObjects.OfType<SliderRepeat>().Single().Position, Is.EqualTo(new Vector2(OsuPlayfield.BASE_SIZE.X - 0, 128)));
|
||||
Assert.That(slider.Path.ControlPoints.Select(point => point.Position), Is.EquivalentTo(new[]
|
||||
{
|
||||
new Vector2(),
|
||||
new Vector2(64, -128),
|
||||
new Vector2(128, 0)
|
||||
}));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestReflectSliderVerticallyAlongPlayfield()
|
||||
{
|
||||
var slider = createTestSlider();
|
||||
|
||||
OsuHitObjectGenerationUtils.ReflectVerticallyAlongPlayfield(slider);
|
||||
|
||||
Assert.That(slider.Position, Is.EqualTo(new Vector2(128, OsuPlayfield.BASE_SIZE.Y - 128)));
|
||||
Assert.That(slider.NestedHitObjects.OfType<SliderRepeat>().Single().Position, Is.EqualTo(new Vector2(0, OsuPlayfield.BASE_SIZE.Y - 128)));
|
||||
Assert.That(slider.Path.ControlPoints.Select(point => point.Position), Is.EquivalentTo(new[]
|
||||
{
|
||||
new Vector2(),
|
||||
new Vector2(-64, 128),
|
||||
new Vector2(-128, 0)
|
||||
}));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFlipSliderInPlaceHorizontally()
|
||||
{
|
||||
var slider = createTestSlider();
|
||||
|
||||
OsuHitObjectGenerationUtils.FlipSliderInPlaceHorizontally(slider);
|
||||
|
||||
Assert.That(slider.Position, Is.EqualTo(new Vector2(128, 128)));
|
||||
Assert.That(slider.NestedHitObjects.OfType<SliderRepeat>().Single().Position, Is.EqualTo(new Vector2(256, 128)));
|
||||
Assert.That(slider.Path.ControlPoints.Select(point => point.Position), Is.EquivalentTo(new[]
|
||||
{
|
||||
new Vector2(),
|
||||
new Vector2(64, -128),
|
||||
new Vector2(128, 0)
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
@ -4,6 +4,8 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
@ -22,6 +24,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
|
||||
|
||||
private bool isPlacingEnd;
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
[CanBeNull]
|
||||
private IBeatSnapProvider beatSnapProvider { get; set; }
|
||||
|
||||
public SpinnerPlacementBlueprint()
|
||||
: base(new Spinner { Position = OsuPlayfield.BASE_SIZE / 2 })
|
||||
{
|
||||
@ -33,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
|
||||
base.Update();
|
||||
|
||||
if (isPlacingEnd)
|
||||
HitObject.EndTime = Math.Max(HitObject.StartTime, EditorClock.CurrentTime);
|
||||
updateEndTimeFromCurrent();
|
||||
|
||||
piece.UpdateFrom(HitObject);
|
||||
}
|
||||
@ -45,7 +51,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
|
||||
if (e.Button != MouseButton.Right)
|
||||
return false;
|
||||
|
||||
HitObject.EndTime = EditorClock.CurrentTime;
|
||||
updateEndTimeFromCurrent();
|
||||
EndPlacement(true);
|
||||
}
|
||||
else
|
||||
@ -61,5 +67,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void updateEndTimeFromCurrent()
|
||||
{
|
||||
HitObject.EndTime = beatSnapProvider == null
|
||||
? Math.Max(HitObject.StartTime, EditorClock.CurrentTime)
|
||||
: Math.Max(HitObject.StartTime + beatSnapProvider.GetBeatLengthAtTime(HitObject.StartTime), beatSnapProvider.SnapTime(EditorClock.CurrentTime));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
var osuObject = (OsuHitObject)hitObject;
|
||||
|
||||
OsuHitObjectGenerationUtils.ReflectVertically(osuObject);
|
||||
OsuHitObjectGenerationUtils.ReflectVerticallyAlongPlayfield(osuObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -27,16 +27,16 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
switch (Reflection.Value)
|
||||
{
|
||||
case MirrorType.Horizontal:
|
||||
OsuHitObjectGenerationUtils.ReflectHorizontally(osuObject);
|
||||
OsuHitObjectGenerationUtils.ReflectHorizontallyAlongPlayfield(osuObject);
|
||||
break;
|
||||
|
||||
case MirrorType.Vertical:
|
||||
OsuHitObjectGenerationUtils.ReflectVertically(osuObject);
|
||||
OsuHitObjectGenerationUtils.ReflectVerticallyAlongPlayfield(osuObject);
|
||||
break;
|
||||
|
||||
case MirrorType.Both:
|
||||
OsuHitObjectGenerationUtils.ReflectHorizontally(osuObject);
|
||||
OsuHitObjectGenerationUtils.ReflectVertically(osuObject);
|
||||
OsuHitObjectGenerationUtils.ReflectHorizontallyAlongPlayfield(osuObject);
|
||||
OsuHitObjectGenerationUtils.ReflectVerticallyAlongPlayfield(osuObject);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -65,6 +65,11 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
flowDirection = !flowDirection;
|
||||
}
|
||||
|
||||
if (positionInfos[i].HitObject is Slider slider && random.NextDouble() < 0.5)
|
||||
{
|
||||
OsuHitObjectGenerationUtils.FlipSliderInPlaceHorizontally(slider);
|
||||
}
|
||||
|
||||
if (i == 0)
|
||||
{
|
||||
positionInfos[i].DistanceFromPrevious = (float)(random.NextDouble() * OsuPlayfield.BASE_SIZE.Y / 2);
|
||||
|
@ -17,7 +17,6 @@ using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||
@ -196,8 +195,8 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
private IEnumerable<double> generateBeats(IBeatmap beatmap, IReadOnlyCollection<OsuHitObject> originalHitObjects)
|
||||
{
|
||||
double startTime = originalHitObjects.First().StartTime;
|
||||
double endTime = originalHitObjects.Last().GetEndTime();
|
||||
double startTime = beatmap.HitObjects.First().StartTime;
|
||||
double endTime = beatmap.GetLastObjectTime();
|
||||
|
||||
var beats = beatmap.ControlPointInfo.TimingPoints
|
||||
// Ignore timing points after endTime
|
||||
|
@ -112,44 +112,46 @@ namespace osu.Game.Rulesets.Osu.Utils
|
||||
/// Reflects the position of the <see cref="OsuHitObject"/> in the playfield horizontally.
|
||||
/// </summary>
|
||||
/// <param name="osuObject">The object to reflect.</param>
|
||||
public static void ReflectHorizontally(OsuHitObject osuObject)
|
||||
public static void ReflectHorizontallyAlongPlayfield(OsuHitObject osuObject)
|
||||
{
|
||||
osuObject.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - osuObject.X, osuObject.Position.Y);
|
||||
|
||||
if (!(osuObject is Slider slider))
|
||||
if (osuObject is not Slider slider)
|
||||
return;
|
||||
|
||||
// No need to update the head and tail circles, since slider handles that when the new slider path is set
|
||||
slider.NestedHitObjects.OfType<SliderTick>().ForEach(h => h.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - h.Position.X, h.Position.Y));
|
||||
slider.NestedHitObjects.OfType<SliderRepeat>().ForEach(h => h.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - h.Position.X, h.Position.Y));
|
||||
void reflectNestedObject(OsuHitObject nested) => nested.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - nested.Position.X, nested.Position.Y);
|
||||
static void reflectControlPoint(PathControlPoint point) => point.Position = new Vector2(-point.Position.X, point.Position.Y);
|
||||
|
||||
var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray();
|
||||
foreach (var point in controlPoints)
|
||||
point.Position = new Vector2(-point.Position.X, point.Position.Y);
|
||||
|
||||
slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value);
|
||||
modifySlider(slider, reflectNestedObject, reflectControlPoint);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reflects the position of the <see cref="OsuHitObject"/> in the playfield vertically.
|
||||
/// </summary>
|
||||
/// <param name="osuObject">The object to reflect.</param>
|
||||
public static void ReflectVertically(OsuHitObject osuObject)
|
||||
public static void ReflectVerticallyAlongPlayfield(OsuHitObject osuObject)
|
||||
{
|
||||
osuObject.Position = new Vector2(osuObject.Position.X, OsuPlayfield.BASE_SIZE.Y - osuObject.Y);
|
||||
|
||||
if (!(osuObject is Slider slider))
|
||||
if (osuObject is not Slider slider)
|
||||
return;
|
||||
|
||||
// No need to update the head and tail circles, since slider handles that when the new slider path is set
|
||||
slider.NestedHitObjects.OfType<SliderTick>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
|
||||
slider.NestedHitObjects.OfType<SliderRepeat>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
|
||||
void reflectNestedObject(OsuHitObject nested) => nested.Position = new Vector2(nested.Position.X, OsuPlayfield.BASE_SIZE.Y - nested.Position.Y);
|
||||
static void reflectControlPoint(PathControlPoint point) => point.Position = new Vector2(point.Position.X, -point.Position.Y);
|
||||
|
||||
var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray();
|
||||
foreach (var point in controlPoints)
|
||||
point.Position = new Vector2(point.Position.X, -point.Position.Y);
|
||||
modifySlider(slider, reflectNestedObject, reflectControlPoint);
|
||||
}
|
||||
|
||||
slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value);
|
||||
/// <summary>
|
||||
/// Flips the position of the <see cref="Slider"/> around its start position horizontally.
|
||||
/// </summary>
|
||||
/// <param name="slider">The slider to be flipped.</param>
|
||||
public static void FlipSliderInPlaceHorizontally(Slider slider)
|
||||
{
|
||||
void flipNestedObject(OsuHitObject nested) => nested.Position = new Vector2(slider.X - (nested.X - slider.X), nested.Y);
|
||||
static void flipControlPoint(PathControlPoint point) => point.Position = new Vector2(-point.Position.X, point.Position.Y);
|
||||
|
||||
modifySlider(slider, flipNestedObject, flipControlPoint);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -160,14 +162,20 @@ namespace osu.Game.Rulesets.Osu.Utils
|
||||
public static void RotateSlider(Slider slider, float rotation)
|
||||
{
|
||||
void rotateNestedObject(OsuHitObject nested) => nested.Position = rotateVector(nested.Position - slider.Position, rotation) + slider.Position;
|
||||
void rotateControlPoint(PathControlPoint point) => point.Position = rotateVector(point.Position, rotation);
|
||||
|
||||
modifySlider(slider, rotateNestedObject, rotateControlPoint);
|
||||
}
|
||||
|
||||
private static void modifySlider(Slider slider, Action<OsuHitObject> modifyNestedObject, Action<PathControlPoint> modifyControlPoint)
|
||||
{
|
||||
// No need to update the head and tail circles, since slider handles that when the new slider path is set
|
||||
slider.NestedHitObjects.OfType<SliderTick>().ForEach(rotateNestedObject);
|
||||
slider.NestedHitObjects.OfType<SliderRepeat>().ForEach(rotateNestedObject);
|
||||
slider.NestedHitObjects.OfType<SliderTick>().ForEach(modifyNestedObject);
|
||||
slider.NestedHitObjects.OfType<SliderRepeat>().ForEach(modifyNestedObject);
|
||||
|
||||
var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray();
|
||||
foreach (var point in controlPoints)
|
||||
point.Position = rotateVector(point.Position, rotation);
|
||||
modifyControlPoint(point);
|
||||
|
||||
slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value);
|
||||
}
|
||||
|
@ -0,0 +1,37 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Taiko.Skinning.Legacy;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Tests.Skinning
|
||||
{
|
||||
public partial class TestSceneTaikoKiaiGlow : TaikoSkinnableTestScene
|
||||
{
|
||||
[Test]
|
||||
public void TestKiaiGlow()
|
||||
{
|
||||
AddStep("Create kiai glow", () => SetContents(_ => new LegacyKiaiGlow()));
|
||||
AddToggleStep("Toggle kiai mode", setUpBeatmap);
|
||||
}
|
||||
|
||||
private void setUpBeatmap(bool withKiai)
|
||||
{
|
||||
var controlPointInfo = new ControlPointInfo();
|
||||
|
||||
controlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 });
|
||||
|
||||
if (withKiai)
|
||||
controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true });
|
||||
|
||||
Beatmap.Value = CreateWorkingBeatmap(new Beatmap
|
||||
{
|
||||
ControlPointInfo = controlPointInfo
|
||||
});
|
||||
|
||||
Beatmap.Value.Track.Start();
|
||||
}
|
||||
}
|
||||
}
|
65
osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyKiaiGlow.cs
Normal file
65
osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyKiaiGlow.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Graphics;
|
||||
@ -16,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
{
|
||||
private Sprite kiai = null!;
|
||||
|
||||
private bool kiaiDisplayed;
|
||||
private bool isKiaiActive;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ISkinSource skin)
|
||||
@ -41,17 +42,19 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
};
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (isKiaiActive)
|
||||
kiai.Alpha = (float)Math.Min(1, kiai.Alpha + Math.Abs(Clock.ElapsedFrameTime) / 200f);
|
||||
else
|
||||
kiai.Alpha = (float)Math.Max(0, kiai.Alpha - Math.Abs(Clock.ElapsedFrameTime) / 200f);
|
||||
}
|
||||
|
||||
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
|
||||
{
|
||||
base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes);
|
||||
|
||||
if (effectPoint.KiaiMode != kiaiDisplayed)
|
||||
{
|
||||
kiaiDisplayed = effectPoint.KiaiMode;
|
||||
|
||||
kiai.ClearTransforms();
|
||||
kiai.FadeTo(kiaiDisplayed ? 1 : 0, 200);
|
||||
}
|
||||
isKiaiActive = effectPoint.KiaiMode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -129,6 +129,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
case TaikoSkinComponents.Mascot:
|
||||
return new DrawableTaikoMascot();
|
||||
|
||||
case TaikoSkinComponents.KiaiGlow:
|
||||
if (GetTexture("taiko-glow") != null)
|
||||
return new LegacyKiaiGlow();
|
||||
|
||||
return null;
|
||||
|
||||
default:
|
||||
throw new UnsupportedSkinComponentException(lookup);
|
||||
}
|
||||
|
@ -21,5 +21,6 @@ namespace osu.Game.Rulesets.Taiko
|
||||
TaikoExplosionKiai,
|
||||
Scroller,
|
||||
Mascot,
|
||||
KiaiGlow
|
||||
}
|
||||
}
|
||||
|
@ -112,6 +112,10 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
FillMode = FillMode.Fit,
|
||||
Children = new[]
|
||||
{
|
||||
new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.KiaiGlow), _ => Empty())
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
hitExplosionContainer = new Container<HitExplosion>
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
|
@ -314,6 +314,24 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGetLastObjectTime()
|
||||
{
|
||||
var decoder = new LegacyBeatmapDecoder();
|
||||
|
||||
using (var resStream = TestResources.OpenResource("mania-last-object-not-latest.osu"))
|
||||
using (var stream = new LineBufferedReader(resStream))
|
||||
{
|
||||
var beatmap = decoder.Decode(stream);
|
||||
|
||||
Assert.That(beatmap.HitObjects.Last().StartTime, Is.EqualTo(2494));
|
||||
Assert.That(beatmap.HitObjects.Last().GetEndTime(), Is.EqualTo(2494));
|
||||
|
||||
Assert.That(beatmap.HitObjects.Max(h => h.GetEndTime()), Is.EqualTo(2582));
|
||||
Assert.That(beatmap.GetLastObjectTime(), Is.EqualTo(2582));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDecodeBeatmapComboOffsetsOsu()
|
||||
{
|
||||
|
@ -16,7 +16,9 @@ using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Catch;
|
||||
using osu.Game.Rulesets.Mania;
|
||||
using osu.Game.Rulesets.Mania.Mods;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Replays;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
@ -179,6 +181,40 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSoloScoreData()
|
||||
{
|
||||
var ruleset = new OsuRuleset().RulesetInfo;
|
||||
|
||||
var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
|
||||
scoreInfo.Mods = new Mod[]
|
||||
{
|
||||
new OsuModDoubleTime { SpeedChange = { Value = 1.1 } }
|
||||
};
|
||||
|
||||
var beatmap = new TestBeatmap(ruleset);
|
||||
var score = new Score
|
||||
{
|
||||
ScoreInfo = scoreInfo,
|
||||
Replay = new Replay
|
||||
{
|
||||
Frames = new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(2000, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(decodedAfterEncode.ScoreInfo.Statistics, Is.EqualTo(scoreInfo.Statistics));
|
||||
Assert.That(decodedAfterEncode.ScoreInfo.MaximumStatistics, Is.EqualTo(scoreInfo.MaximumStatistics));
|
||||
Assert.That(decodedAfterEncode.ScoreInfo.Mods, Is.EqualTo(scoreInfo.Mods));
|
||||
});
|
||||
}
|
||||
|
||||
private static Score encodeThenDecode(int beatmapVersion, Score score, TestBeatmap beatmap)
|
||||
{
|
||||
var encodeStream = new MemoryStream();
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
39
osu.Game.Tests/Resources/mania-last-object-not-latest.osu
Normal file
39
osu.Game.Tests/Resources/mania-last-object-not-latest.osu
Normal 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:
|
@ -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();
|
||||
|
@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -261,7 +261,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Test]
|
||||
public void TestFinalFramesPurgedBeforeEndingPlay()
|
||||
{
|
||||
AddStep("begin playing", () => spectatorClient.BeginPlaying(TestGameplayState.Create(new OsuRuleset()), new Score()));
|
||||
AddStep("begin playing", () => spectatorClient.BeginPlaying(0, TestGameplayState.Create(new OsuRuleset()), new Score()));
|
||||
|
||||
AddStep("send frames and finish play", () =>
|
||||
{
|
||||
|
@ -147,7 +147,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
}
|
||||
};
|
||||
|
||||
spectatorClient.BeginPlaying(TestGameplayState.Create(new OsuRuleset()), recordingScore);
|
||||
spectatorClient.BeginPlaying(0, TestGameplayState.Create(new OsuRuleset()), recordingScore);
|
||||
spectatorClient.OnNewFrames += onNewFrames;
|
||||
});
|
||||
}
|
||||
|
@ -117,11 +117,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
BeatmapID = 0,
|
||||
RulesetID = 0,
|
||||
Mods = user.Mods,
|
||||
MaximumScoringValues = new ScoringValues
|
||||
MaximumStatistics = new Dictionary<HitResult, int>
|
||||
{
|
||||
BaseScore = 10000,
|
||||
MaxCombo = 1000,
|
||||
CountBasicHitObjects = 1000
|
||||
{ HitResult.Perfect, 100 }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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]
|
||||
|
129
osu.Game.Tests/Visual/UserInterface/TestSceneButtonsInput.cs
Normal file
129
osu.Game.Tests/Visual/UserInterface/TestSceneButtonsInput.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -126,6 +126,21 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
checkBindableAtValue("Circle Size", 9);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestExtendedLimitsRetainedAfterBoundCopyCreation()
|
||||
{
|
||||
setExtendedLimits(true);
|
||||
setSliderValue("Circle Size", 11);
|
||||
|
||||
checkSliderAtValue("Circle Size", 11);
|
||||
checkBindableAtValue("Circle Size", 11);
|
||||
|
||||
AddStep("create bound copy", () => _ = modDifficultyAdjust.CircleSize.GetBoundCopy());
|
||||
|
||||
checkSliderAtValue("Circle Size", 11);
|
||||
checkBindableAtValue("Circle Size", 11);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestResetToDefault()
|
||||
{
|
||||
|
@ -1,47 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
public partial class TestSceneOsuButton : OsuTestScene
|
||||
{
|
||||
[Test]
|
||||
public void TestToggleEnabled()
|
||||
{
|
||||
OsuButton button = null;
|
||||
|
||||
AddStep("add button", () => Child = button = new OsuButton
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(200),
|
||||
Text = "Button"
|
||||
});
|
||||
|
||||
AddToggleStep("toggle enabled", toggle =>
|
||||
{
|
||||
for (int i = 0; i < 6; i++)
|
||||
button.Action = toggle ? () => { } : null;
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestInitiallyDisabled()
|
||||
{
|
||||
AddStep("add button", () => Child = new OsuButton
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(200),
|
||||
Text = "Button"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -109,6 +109,8 @@ namespace osu.Game.Audio
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
Stop();
|
||||
Track?.Dispose();
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -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").
|
||||
|
@ -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.
|
||||
|
25
osu.Game/Database/ImportParameters.cs
Normal file
25
osu.Game/Database/ImportParameters.cs
Normal 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; }
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -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()))
|
||||
|
@ -1,14 +1,13 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Graphics.Containers
|
||||
{
|
||||
@ -18,6 +17,12 @@ namespace osu.Game.Graphics.Containers
|
||||
|
||||
private readonly Container content = new Container { RelativeSizeAxes = Axes.Both };
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
|
||||
// base call is checked for cases when `OsuClickableContainer` has masking applied to it directly (ie. externally in object initialisation).
|
||||
base.ReceivePositionalInputAt(screenSpacePos)
|
||||
// Implementations often apply masking / edge rounding at a content level, so it's imperative to check that as well.
|
||||
&& Content.ReceivePositionalInputAt(screenSpacePos);
|
||||
|
||||
protected override Container<Drawable> Content => content;
|
||||
|
||||
protected virtual HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverClickSounds(sampleSet) { Enabled = { BindTarget = Enabled } };
|
||||
@ -38,11 +43,8 @@ namespace osu.Game.Graphics.Containers
|
||||
content.AutoSizeAxes = AutoSizeAxes;
|
||||
}
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
content,
|
||||
CreateHoverSounds(sampleSet)
|
||||
};
|
||||
AddInternal(content);
|
||||
Add(CreateHoverSounds(sampleSet));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -240,7 +240,9 @@ namespace osu.Game.Graphics.Containers
|
||||
headerBackgroundContainer.Height = expandableHeaderSize + fixedHeaderSize;
|
||||
headerBackgroundContainer.Y = ExpandableHeader?.Y ?? 0;
|
||||
|
||||
float smallestSectionHeight = Children.Count > 0 ? Children.Min(d => d.Height) : 0;
|
||||
var flowChildren = scrollContentContainer.FlowingChildren.OfType<T>();
|
||||
|
||||
float smallestSectionHeight = flowChildren.Any() ? flowChildren.Min(d => d.Height) : 0;
|
||||
|
||||
// scroll offset is our fixed header height if we have it plus 10% of content height
|
||||
// plus 5% to fix floating point errors and to not have a section instantly unselect when scrolling upwards
|
||||
@ -249,7 +251,7 @@ namespace osu.Game.Graphics.Containers
|
||||
|
||||
float scrollCentre = fixedHeaderSize + scrollContainer.DisplayableContent * scroll_y_centre + selectionLenienceAboveSection;
|
||||
|
||||
var presentChildren = Children.Where(c => c.IsPresent);
|
||||
var presentChildren = flowChildren.Where(c => c.IsPresent);
|
||||
|
||||
if (lastClickedSection != null)
|
||||
SelectedSection.Value = lastClickedSection;
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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]
|
||||
|
@ -197,7 +197,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
}, true);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(OverlayColourProvider? colourProvider)
|
||||
{
|
||||
if (colourProvider == null) return;
|
||||
|
108
osu.Game/IO/HardLinkHelper.cs
Normal file
108
osu.Game/IO/HardLinkHelper.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
33
osu.Game/Localisation/BeatmapOverlayStrings.cs
Normal file
33
osu.Game/Localisation/BeatmapOverlayStrings.cs
Normal 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 "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>
|
||||
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}";
|
||||
}
|
||||
}
|
@ -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"
|
||||
|
@ -32,6 +32,7 @@ namespace osu.Game.Online.API.Requests
|
||||
Loved,
|
||||
Pending,
|
||||
Guest,
|
||||
Graveyard
|
||||
Graveyard,
|
||||
Nominated,
|
||||
}
|
||||
}
|
||||
|
@ -164,6 +164,9 @@ namespace osu.Game.Online.API.Requests.Responses
|
||||
[JsonProperty(@"guest_beatmapset_count")]
|
||||
public int GuestBeatmapsetCount;
|
||||
|
||||
[JsonProperty(@"nominated_beatmapset_count")]
|
||||
public int NominatedBeatmapsetCount;
|
||||
|
||||
[JsonProperty(@"scores_best_count")]
|
||||
public int ScoresBestCount;
|
||||
|
||||
|
@ -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 =>
|
||||
{
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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}";
|
||||
}
|
||||
}
|
||||
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -15,8 +15,9 @@ namespace osu.Game.Online.Spectator
|
||||
/// <summary>
|
||||
/// Signal the start of a new play session.
|
||||
/// </summary>
|
||||
/// <param name="scoreToken">The score submission token.</param>
|
||||
/// <param name="state">The state of gameplay.</param>
|
||||
Task BeginPlaySession(SpectatorState state);
|
||||
Task BeginPlaySession(long? scoreToken, SpectatorState state);
|
||||
|
||||
/// <summary>
|
||||
/// Send a bundle of frame data for the current play session.
|
||||
|
@ -47,7 +47,7 @@ namespace osu.Game.Online.Spectator
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task BeginPlayingInternal(SpectatorState state)
|
||||
protected override async Task BeginPlayingInternal(long? scoreToken, SpectatorState state)
|
||||
{
|
||||
if (!IsConnected.Value)
|
||||
return;
|
||||
@ -56,7 +56,7 @@ namespace osu.Game.Online.Spectator
|
||||
|
||||
try
|
||||
{
|
||||
await connection.InvokeAsync(nameof(ISpectatorServer.BeginPlaySession), state);
|
||||
await connection.InvokeAsync(nameof(ISpectatorServer.BeginPlaySession), scoreToken, state);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
@ -65,7 +65,7 @@ namespace osu.Game.Online.Spectator
|
||||
Debug.Assert(connector != null);
|
||||
|
||||
await connector.Reconnect();
|
||||
await BeginPlayingInternal(state);
|
||||
await BeginPlayingInternal(scoreToken, state);
|
||||
}
|
||||
|
||||
// Exceptions can occur if, for instance, the locally played beatmap doesn't have a server-side counterpart.
|
||||
|
@ -76,6 +76,7 @@ namespace osu.Game.Online.Spectator
|
||||
|
||||
private IBeatmap? currentBeatmap;
|
||||
private Score? currentScore;
|
||||
private long? currentScoreToken;
|
||||
|
||||
private readonly Queue<FrameDataBundle> pendingFrameBundles = new Queue<FrameDataBundle>();
|
||||
|
||||
@ -108,7 +109,7 @@ namespace osu.Game.Online.Spectator
|
||||
// re-send state in case it wasn't received
|
||||
if (IsPlaying)
|
||||
// TODO: this is likely sent out of order after a reconnect scenario. needs further consideration.
|
||||
BeginPlayingInternal(currentState);
|
||||
BeginPlayingInternal(currentScoreToken, currentState);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -159,7 +160,7 @@ namespace osu.Game.Online.Spectator
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void BeginPlaying(GameplayState state, Score score)
|
||||
public void BeginPlaying(long? scoreToken, GameplayState state, Score score)
|
||||
{
|
||||
// This schedule is only here to match the one below in `EndPlaying`.
|
||||
Schedule(() =>
|
||||
@ -174,12 +175,13 @@ namespace osu.Game.Online.Spectator
|
||||
currentState.RulesetID = score.ScoreInfo.RulesetID;
|
||||
currentState.Mods = score.ScoreInfo.Mods.Select(m => new APIMod(m)).ToArray();
|
||||
currentState.State = SpectatedUserState.Playing;
|
||||
currentState.MaximumScoringValues = state.ScoreProcessor.MaximumScoringValues;
|
||||
currentState.MaximumStatistics = state.ScoreProcessor.MaximumStatistics;
|
||||
|
||||
currentBeatmap = state.Beatmap;
|
||||
currentScore = score;
|
||||
currentScoreToken = scoreToken;
|
||||
|
||||
BeginPlayingInternal(currentState);
|
||||
BeginPlayingInternal(currentScoreToken, currentState);
|
||||
});
|
||||
}
|
||||
|
||||
@ -264,7 +266,7 @@ namespace osu.Game.Online.Spectator
|
||||
});
|
||||
}
|
||||
|
||||
protected abstract Task BeginPlayingInternal(SpectatorState state);
|
||||
protected abstract Task BeginPlayingInternal(long? scoreToken, SpectatorState state);
|
||||
|
||||
protected abstract Task SendFramesInternal(FrameDataBundle bundle);
|
||||
|
||||
|
@ -152,12 +152,12 @@ namespace osu.Game.Online.Spectator
|
||||
|
||||
scoreInfo.MaxCombo = frame.Header.MaxCombo;
|
||||
scoreInfo.Statistics = frame.Header.Statistics;
|
||||
scoreInfo.MaximumStatistics = spectatorState.MaximumStatistics;
|
||||
|
||||
Accuracy.Value = frame.Header.Accuracy;
|
||||
Combo.Value = frame.Header.Combo;
|
||||
|
||||
scoreProcessor.ExtractScoringValues(frame.Header, out var currentScoringValues, out _);
|
||||
TotalScore.Value = scoreProcessor.ComputeScore(Mode.Value, currentScoringValues, spectatorState.MaximumScoringValues);
|
||||
TotalScore.Value = scoreProcessor.ComputeScore(Mode.Value, scoreInfo);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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());
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -146,6 +146,7 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
}
|
||||
});
|
||||
|
||||
generalFilter.Current.Add(SearchGeneral.FeaturedArtists);
|
||||
categoryFilter.Current.Value = SearchCategory.Leaderboard;
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
});
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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));
|
||||
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -198,6 +198,7 @@ namespace osu.Game.Overlays.Dialog
|
||||
TextAnchor = Anchor.TopCentre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Padding = new MarginPadding(5),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -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);
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user