1
0
mirror of https://github.com/ppy/osu.git synced 2025-03-23 08:27:23 +08:00

Merge branch 'master' into editor-storyboard-display-2

This commit is contained in:
Bartłomiej Dach 2025-01-08 08:12:55 +01:00
commit d9f0f0d729
No known key found for this signature in database
75 changed files with 803 additions and 332 deletions

View File

@ -51,8 +51,11 @@ dotnet_diagnostic.IDE1006.severity = warning
# Too many noisy warnings for parsing/formatting numbers
dotnet_diagnostic.CA1305.severity = none
# messagepack complains about "osu" not being title cased due to reserved words
dotnet_diagnostic.CS8981.severity = none
# CA1507: Use nameof to express symbol names
# Flaggs serialization name attributes
# Flags serialization name attributes
dotnet_diagnostic.CA1507.severity = suggestion
# CA1806: Do not ignore method results

View File

@ -9,9 +9,9 @@
<GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\osu.Game.Rulesets.EmptyFreeform\osu.Game.Rulesets.EmptyFreeform.csproj" />

View File

@ -14,7 +14,16 @@ namespace osu.Game.Rulesets.EmptyFreeform.Objects
public Vector2 Position { get; set; }
public float X => Position.X;
public float Y => Position.Y;
public float X
{
get => Position.X;
set => Position = new Vector2(value, Y);
}
public float Y
{
get => Position.Y;
set => Position = new Vector2(X, value);
}
}
}

View File

@ -9,9 +9,9 @@
<GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />

View File

@ -14,7 +14,16 @@ namespace osu.Game.Rulesets.Pippidon.Objects
public Vector2 Position { get; set; }
public float X => Position.X;
public float Y => Position.Y;
public float X
{
get => Position.X;
set => Position = new Vector2(value, Y);
}
public float Y
{
get => Position.Y;
set => Position = new Vector2(X, value);
}
}
}

View File

@ -9,9 +9,9 @@
<GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\osu.Game.Rulesets.EmptyScrolling\osu.Game.Rulesets.EmptyScrolling.csproj" />

View File

@ -9,9 +9,9 @@
<GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />

View File

@ -24,9 +24,9 @@
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="System.IO.Packaging" Version="8.0.1" />
<PackageReference Include="System.IO.Packaging" Version="9.0.0" />
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
<PackageReference Include="Velopack" Version="0.0.915" />
<PackageReference Include="Velopack" Version="0.0.1053" />
</ItemGroup>
<ItemGroup Label="Resources">
<EmbeddedResource Include="lazer.ico" />

View File

@ -9,7 +9,7 @@
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
<PackageReference Include="nunit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
</ItemGroup>
<ItemGroup>

View File

@ -1,9 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
</ItemGroup>
<PropertyGroup Label="Project">
<OutputType>WinExe</OutputType>

View File

@ -210,11 +210,27 @@ namespace osu.Game.Rulesets.Catch.Objects
/// </summary>
public float LegacyConvertedY { get; set; } = DEFAULT_LEGACY_CONVERT_Y;
float IHasXPosition.X => OriginalX;
float IHasXPosition.X
{
get => OriginalX;
set => OriginalX = value;
}
float IHasYPosition.Y => LegacyConvertedY;
float IHasYPosition.Y
{
get => LegacyConvertedY;
set => LegacyConvertedY = value;
}
Vector2 IHasPosition.Position => new Vector2(OriginalX, LegacyConvertedY);
Vector2 IHasPosition.Position
{
get => new Vector2(OriginalX, LegacyConvertedY);
set
{
((IHasXPosition)this).X = value.X;
((IHasYPosition)this).Y = value.Y;
}
}
#endregion
}

View File

@ -1,9 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
</ItemGroup>
<PropertyGroup Label="Project">
<OutputType>WinExe</OutputType>

View File

@ -25,7 +25,11 @@ namespace osu.Game.Rulesets.Mania.Objects
#region LegacyBeatmapEncoder
float IHasXPosition.X => Column;
float IHasXPosition.X
{
get => Column;
set => Column = (int)value;
}
#endregion
}

View File

@ -1,10 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
</ItemGroup>
<PropertyGroup Label="Project">
<OutputType>WinExe</OutputType>

View File

@ -626,7 +626,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
{
if (BodyPiece.ReceivePositionalInputAt(screenSpacePos))
if (BodyPiece.ReceivePositionalInputAt(screenSpacePos) && DrawableObject.Body.Alpha > 0)
return true;
if (ControlPointVisualiser == null)

View File

@ -230,7 +230,7 @@ namespace osu.Game.Rulesets.Osu.Mods
// If samples aren't available at the exact start time of the object,
// use samples (without additions) in the closest original hit object instead
obj.Samples = samples ?? getClosestHitObject(originalHitObjects, obj.StartTime).Samples.Where(s => !HitSampleInfo.AllAdditions.Contains(s.Name)).ToList();
obj.Samples = samples ?? getClosestHitObject(originalHitObjects, obj.StartTime).Samples.Where(s => !HitSampleInfo.ALL_ADDITIONS.Contains(s.Name)).ToList();
}
}

View File

@ -377,13 +377,34 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
UpdateState(ArmedState.Idle);
HeadCircle.SuppressHitAnimations();
foreach (var repeat in repeatContainer)
repeat.SuppressHitAnimations();
TailCircle.SuppressHitAnimations();
// This method is called every frame in editor contexts, thus the lack of need for transforms.
if (Time.Current >= HitStateUpdateTime)
{
// Apply the slider's alpha to *only* the body.
// This allows start and more importantly end circles to fade slower than the overall slider.
if (Alpha < 1)
Body.Alpha = Alpha;
Alpha = 1;
}
LifetimeEnd = HitStateUpdateTime + 700;
}
internal void RestoreHitAnimations()
{
UpdateState(ArmedState.Hit);
HeadCircle.RestoreHitAnimations();
foreach (var repeat in repeatContainer)
repeat.RestoreHitAnimations();
TailCircle.RestoreHitAnimations();
}

View File

@ -14,6 +14,7 @@ using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
@ -163,5 +164,37 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Arrow.Rotation = Interpolation.ValueAt(Math.Clamp(Clock.ElapsedFrameTime, 0, 100), Arrow.Rotation, aimRotation, 0, 50, Easing.OutQuint);
}
}
#region FOR EDITOR USE ONLY, DO NOT USE FOR ANY OTHER PURPOSE
internal void SuppressHitAnimations()
{
UpdateState(ArmedState.Idle);
UpdateComboColour();
// This method is called every frame in editor contexts, thus the lack of need for transforms.
bool hit = Time.Current >= HitStateUpdateTime;
if (hit)
{
// More or less matches stable (see https://github.com/peppy/osu-stable-reference/blob/bb57924c1552adbed11ee3d96cdcde47cf96f2b6/osu!/GameplayElements/HitObjects/Osu/HitCircleOsu.cs#L336-L338)
AccentColour.Value = Color4.White;
Alpha = Interpolation.ValueAt(Time.Current, 1f, 0f, HitStateUpdateTime, HitStateUpdateTime + 700);
}
Arrow.Alpha = hit ? 0 : 1;
LifetimeEnd = HitStateUpdateTime + 700;
}
internal void RestoreHitAnimations()
{
UpdateState(ArmedState.Hit);
UpdateComboColour();
Arrow.Alpha = 1;
}
#endregion
}
}

View File

@ -59,8 +59,17 @@ namespace osu.Game.Rulesets.Osu.Objects
set => position.Value = value;
}
public float X => Position.X;
public float Y => Position.Y;
public float X
{
get => Position.X;
set => Position = new Vector2(value, Position.Y);
}
public float Y
{
get => Position.Y;
set => Position = new Vector2(Position.X, value);
}
public Vector2 StackedPosition => Position + StackOffset;

View File

@ -5,12 +5,12 @@ using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Utils;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
@ -75,44 +75,38 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
accentColour = drawableRepeat.AccentColour.GetBoundCopy();
accentColour.BindValueChanged(accent => icon.Colour = accent.NewValue.Darken(4), true);
drawableRepeat.ApplyCustomUpdateState += updateStateTransforms;
}
private void updateStateTransforms(DrawableHitObject hitObject, ArmedState state)
protected override void Update()
{
base.Update();
if (Time.Current >= drawableRepeat.HitStateUpdateTime && drawableRepeat.State.Value == ArmedState.Hit)
{
double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration);
Scale = new Vector2(Interpolation.ValueAt(Time.Current, 1, 1.5f, drawableRepeat.HitStateUpdateTime, drawableRepeat.HitStateUpdateTime + animDuration, Easing.Out));
}
else
Scale = Vector2.One;
const float move_distance = -12;
const float scale_amount = 1.3f;
const double move_out_duration = 35;
const double move_in_duration = 250;
const double total = 300;
switch (state)
{
case ArmedState.Idle:
main.ScaleTo(1.3f, move_out_duration, Easing.Out)
.Then()
.ScaleTo(1f, move_in_duration, Easing.Out)
.Loop(total - (move_in_duration + move_out_duration));
side
.MoveToX(move_distance, move_out_duration, Easing.Out)
.Then()
.MoveToX(0, move_in_duration, Easing.Out)
.Loop(total - (move_in_duration + move_out_duration));
break;
double loopCurrentTime = (Time.Current - drawableRepeat.AnimationStartTime.Value) % total;
case ArmedState.Hit:
double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration);
this.ScaleTo(1.5f, animDuration, Easing.Out);
break;
}
}
if (loopCurrentTime < move_out_duration)
main.Scale = new Vector2(Interpolation.ValueAt(loopCurrentTime, 1, scale_amount, 0, move_out_duration, Easing.Out));
else
main.Scale = new Vector2(Interpolation.ValueAt(loopCurrentTime, scale_amount, 1f, move_out_duration, move_out_duration + move_in_duration, Easing.Out));
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (drawableRepeat.IsNotNull())
drawableRepeat.ApplyCustomUpdateState -= updateStateTransforms;
if (loopCurrentTime < move_out_duration)
side.X = Interpolation.ValueAt(loopCurrentTime, 0, move_distance, 0, move_out_duration, Easing.Out);
else
side.X = Interpolation.ValueAt(loopCurrentTime, move_distance, 0, move_out_duration, move_out_duration + move_in_duration, Easing.Out);
}
}
}

View File

@ -3,10 +3,10 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Utils;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
@ -40,37 +40,31 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
private void load(DrawableHitObject drawableObject)
{
drawableRepeat = (DrawableSliderRepeat)drawableObject;
drawableRepeat.ApplyCustomUpdateState += updateStateTransforms;
}
private void updateStateTransforms(DrawableHitObject hitObject, ArmedState state)
protected override void Update()
{
const double move_out_duration = 35;
const double move_in_duration = 250;
const double total = 300;
base.Update();
switch (state)
if (Time.Current >= drawableRepeat.HitStateUpdateTime && drawableRepeat.State.Value == ArmedState.Hit)
{
case ArmedState.Idle:
InternalChild.ScaleTo(1.3f, move_out_duration, Easing.Out)
.Then()
.ScaleTo(1f, move_in_duration, Easing.Out)
.Loop(total - (move_in_duration + move_out_duration));
break;
case ArmedState.Hit:
double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration);
InternalChild.ScaleTo(1.5f, animDuration, Easing.Out);
break;
double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration);
Scale = new Vector2(Interpolation.ValueAt(Time.Current, 1, 1.5f, drawableRepeat.HitStateUpdateTime, drawableRepeat.HitStateUpdateTime + animDuration, Easing.Out));
}
}
else
{
const float scale_amount = 1.3f;
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
const double move_out_duration = 35;
const double move_in_duration = 250;
const double total = 300;
if (drawableRepeat.IsNotNull())
drawableRepeat.ApplyCustomUpdateState -= updateStateTransforms;
double loopCurrentTime = (Time.Current - drawableRepeat.AnimationStartTime.Value) % total;
if (loopCurrentTime < move_out_duration)
Scale = new Vector2(Interpolation.ValueAt(loopCurrentTime, 1, scale_amount, 0, move_out_duration, Easing.Out));
else
Scale = new Vector2(Interpolation.ValueAt(loopCurrentTime, scale_amount, 1f, move_out_duration, move_out_duration + move_in_duration, Easing.Out));
}
}
}
}

View File

@ -9,10 +9,12 @@ using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Utils;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
@ -51,8 +53,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
textureIsDefaultSkin = skin is ISkinTransformer transformer && transformer.Skin is DefaultLegacySkin;
drawableObject.ApplyCustomUpdateState += updateStateTransforms;
shouldRotate = skinSource.GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version)?.Value <= 1;
}
@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
accentColour = drawableRepeat.AccentColour.GetBoundCopy();
accentColour.BindValueChanged(c =>
{
arrow.Colour = textureIsDefaultSkin && c.NewValue.R + c.NewValue.G + c.NewValue.B > (600 / 255f) ? Color4.Black : Color4.White;
arrow.Colour = textureIsDefaultSkin && c.NewValue.R + c.NewValue.G + c.NewValue.B > 600 / 255f ? Color4.Black : Color4.White;
}, true);
}
@ -80,36 +80,32 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
drawableRepeat.DrawableSlider.OverlayElementContainer.Add(proxy);
}
private void updateStateTransforms(DrawableHitObject hitObject, ArmedState state)
protected override void Update()
{
const double duration = 300;
const float rotation = 5.625f;
base.Update();
switch (state)
if (Time.Current >= drawableRepeat.HitStateUpdateTime && drawableRepeat.State.Value == ArmedState.Hit)
{
case ArmedState.Idle:
if (shouldRotate)
{
InternalChild.ScaleTo(1.3f)
.RotateTo(rotation)
.Then()
.ScaleTo(1f, duration)
.RotateTo(-rotation, duration)
.Loop();
}
else
{
InternalChild.ScaleTo(1.3f).Then()
.ScaleTo(1f, duration, Easing.Out)
.Loop();
}
double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration);
arrow.Scale = new Vector2(Interpolation.ValueAt(Time.Current, 1, 1.4f, drawableRepeat.HitStateUpdateTime, drawableRepeat.HitStateUpdateTime + animDuration, Easing.Out));
}
else
{
const double duration = 300;
const float rotation = 5.625f;
break;
double loopCurrentTime = (Time.Current - drawableRepeat.AnimationStartTime.Value) % duration;
case ArmedState.Hit:
double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration);
InternalChild.ScaleTo(1.4f, animDuration, Easing.Out);
break;
// Reference: https://github.com/peppy/osu-stable-reference/blob/2280c4c436f80d04f9c79d3c905db00ac2902273/osu!/GameplayElements/HitObjects/Osu/HitCircleSliderEnd.cs#L79-L96
if (shouldRotate)
{
arrow.Rotation = Interpolation.ValueAt(loopCurrentTime, rotation, -rotation, 0, duration);
arrow.Scale = new Vector2(Interpolation.ValueAt(loopCurrentTime, 1.3f, 1, 0, duration));
}
else
{
arrow.Scale = new Vector2(Interpolation.ValueAt(loopCurrentTime, 1.3f, 1, 0, duration, Easing.Out));
}
}
}
@ -120,7 +116,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
if (drawableRepeat.IsNotNull())
{
drawableRepeat.HitObjectApplied -= onHitObjectApplied;
drawableRepeat.ApplyCustomUpdateState -= updateStateTransforms;
}
}
}

View File

@ -1,9 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
</ItemGroup>
<PropertyGroup Label="Project">
<OutputType>WinExe</OutputType>

View File

@ -11,6 +11,7 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.IO.Archives;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Tests.Resources;
using osu.Game.Tests.Visual;
using MemoryStream = System.IO.MemoryStream;
@ -50,6 +51,29 @@ namespace osu.Game.Tests.Beatmaps.IO
AddAssert("hit object is snapped", () => beatmap.Beatmap.HitObjects[0].StartTime, () => Is.EqualTo(28519).Within(0.001));
}
[Test]
public void TestFractionalObjectCoordinatesRounded()
{
IWorkingBeatmap beatmap = null!;
MemoryStream outStream = null!;
// Ensure importer encoding is correct
AddStep("import beatmap", () => beatmap = importBeatmapFromArchives(@"fractional-coordinates.olz"));
AddAssert("hit object has fractional position", () => ((IHasYPosition)beatmap.Beatmap.HitObjects[1]).Y, () => Is.EqualTo(383.99997).Within(0.00001));
// Ensure exporter legacy conversion is correct
AddStep("export", () =>
{
outStream = new MemoryStream();
new LegacyBeatmapExporter(LocalStorage)
.ExportToStream((BeatmapSetInfo)beatmap.BeatmapInfo.BeatmapSet!, outStream, null);
});
AddStep("import beatmap again", () => beatmap = importBeatmapFromStream(outStream));
AddAssert("hit object is snapped", () => ((IHasYPosition)beatmap.Beatmap.HitObjects[1]).Y, () => Is.EqualTo(384).Within(0.00001));
}
[Test]
public void TestExportStability()
{

View File

@ -112,5 +112,20 @@ namespace osu.Game.Tests.Beatmaps
}
});
}
[Test]
public void TestRepeatsGeneratedEvenForZeroLengthSlider()
{
var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, 0, 2).ToArray();
Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head));
Assert.That(events[0].Time, Is.EqualTo(start_time));
Assert.That(events[1].Type, Is.EqualTo(SliderEventType.Repeat));
Assert.That(events[1].Time, Is.EqualTo(span_duration));
Assert.That(events[3].Type, Is.EqualTo(SliderEventType.Tail));
Assert.That(events[3].Time, Is.EqualTo(span_duration * 2));
}
}
}

View File

@ -27,18 +27,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[SetUpSteps]
public void SetUpSteps()
{
AddStep("Create control", () =>
{
Child = new PlayerSettingsGroup("Some settings")
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
{
offsetControl = new BeatmapOffsetControl()
}
};
});
recreateControl();
}
[Test]
@ -123,13 +112,14 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestCalibrationFromZero()
{
ScoreInfo referenceScore = null!;
const double average_error = -4.5;
AddAssert("Offset is neutral", () => offsetControl.Current.Value == 0);
AddAssert("No calibration button", () => !offsetControl.ChildrenOfType<SettingsButton>().Any());
AddStep("Set reference score", () =>
{
offsetControl.ReferenceScore.Value = new ScoreInfo
offsetControl.ReferenceScore.Value = referenceScore = new ScoreInfo
{
HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(average_error),
BeatmapInfo = Beatmap.Value.BeatmapInfo,
@ -143,6 +133,10 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("Button is disabled", () => !offsetControl.ChildrenOfType<SettingsButton>().Single().Enabled.Value);
AddStep("Remove reference score", () => offsetControl.ReferenceScore.Value = null);
AddAssert("No calibration button", () => !offsetControl.ChildrenOfType<SettingsButton>().Any());
recreateControl();
AddStep("Set same reference score", () => offsetControl.ReferenceScore.Value = referenceScore);
AddAssert("No calibration button", () => !offsetControl.ChildrenOfType<SettingsButton>().Any());
}
/// <summary>
@ -251,5 +245,21 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("Remove reference score", () => offsetControl.ReferenceScore.Value = null);
AddAssert("No calibration button", () => !offsetControl.ChildrenOfType<SettingsButton>().Any());
}
private void recreateControl()
{
AddStep("Create control", () =>
{
Child = new PlayerSettingsGroup("Some settings")
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
{
offsetControl = new BeatmapOffsetControl()
}
};
});
}
}
}

View File

@ -26,7 +26,7 @@ namespace osu.Game.Tests.Visual.Menus
protected OsuScreenStack IntroStack;
private IntroScreen intro;
protected IntroScreen Intro { get; private set; }
[Cached(typeof(INotificationOverlay))]
private NotificationOverlay notifications;
@ -62,22 +62,9 @@ namespace osu.Game.Tests.Visual.Menus
[Test]
public virtual void TestPlayIntro()
{
AddStep("restart sequence", () =>
{
logo.FinishTransforms();
logo.IsTracking = false;
RestartIntro();
IntroStack?.Expire();
Add(IntroStack = new OsuScreenStack
{
RelativeSizeAxes = Axes.Both,
});
IntroStack.Push(intro = CreateScreen());
});
AddUntilStep("wait for menu", () => intro.DidLoadMenu);
WaitForMenu();
}
[Test]
@ -103,18 +90,18 @@ namespace osu.Game.Tests.Visual.Menus
RelativeSizeAxes = Axes.Both,
});
IntroStack.Push(intro = CreateScreen());
IntroStack.Push(Intro = CreateScreen());
});
AddStep("trigger failure", () =>
{
trackResetDelegate = Scheduler.AddDelayed(() =>
{
intro.Beatmap.Value.Track.Seek(0);
Intro.Beatmap.Value.Track.Seek(0);
}, 0, true);
});
AddUntilStep("wait for menu", () => intro.DidLoadMenu);
WaitForMenu();
if (IntroReliesOnTrack)
AddUntilStep("wait for notification", () => notifications.UnreadCount.Value == 1);
@ -122,6 +109,29 @@ namespace osu.Game.Tests.Visual.Menus
AddStep("uninstall delegate", () => trackResetDelegate?.Cancel());
}
protected void RestartIntro()
{
AddStep("restart sequence", () =>
{
logo.FinishTransforms();
logo.IsTracking = false;
IntroStack?.Expire();
Add(IntroStack = new OsuScreenStack
{
RelativeSizeAxes = Axes.Both,
});
IntroStack.Push(Intro = CreateScreen());
});
}
protected void WaitForMenu()
{
AddUntilStep("wait for menu", () => Intro.DidLoadMenu);
}
protected abstract IntroScreen CreateScreen();
}
}

View File

@ -0,0 +1,37 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Screens.Menu;
namespace osu.Game.Tests.Visual.Menus
{
[HeadlessTest]
[TestFixture]
public partial class TestSceneIntroIntegrity : IntroTestScene
{
[Test]
public virtual void TestDeletedFilesRestored()
{
RestartIntro();
WaitForMenu();
AddStep("delete game files unexpectedly", () => LocalStorage.DeleteDirectory("files"));
AddStep("reset game beatmap", () => Dependencies.Get<Bindable<WorkingBeatmap>>().Value = new DummyWorkingBeatmap(Audio, null));
AddStep("invalidate beatmap from cache", () => Dependencies.Get<IWorkingBeatmapCache>().Invalidate(Intro.Beatmap.Value.BeatmapSetInfo));
RestartIntro();
WaitForMenu();
AddUntilStep("ensure track is not virtual", () => Intro.Beatmap.Value.Track is TrackBass);
}
protected override bool IntroReliesOnTrack => true;
protected override IntroScreen CreateScreen() => new IntroTriangles();
}
}

View File

@ -3,29 +3,71 @@
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Tests.Visual.OnlinePlay;
using osu.Game.Tests.Resources;
using osuTK;
namespace osu.Game.Tests.Visual.Multiplayer
{
public partial class TestSceneStarRatingRangeDisplay : OnlinePlayTestScene
public partial class TestSceneStarRatingRangeDisplay : OsuTestScene
{
public override void SetUpSteps()
private readonly Room room = new Room();
protected override void LoadComplete()
{
base.SetUpSteps();
base.LoadComplete();
AddStep("create display", () =>
Child = new FillFlowContainer
{
SelectedRoom.Value = new Room();
Child = new StarRatingRangeDisplay(SelectedRoom.Value)
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(10),
Children = new Drawable[]
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
};
});
new StarRatingRangeDisplay(room)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(5),
},
new StarRatingRangeDisplay(room)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(2),
},
new StarRatingRangeDisplay(room)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(1),
},
new StarRatingRangeDisplay(room)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Alpha = 0.2f,
Scale = new Vector2(5),
},
new StarRatingRangeDisplay(room)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Alpha = 0.2f,
Scale = new Vector2(2),
},
new StarRatingRangeDisplay(room)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Alpha = 0.2f,
Scale = new Vector2(1),
},
}
};
}
[Test]
@ -33,10 +75,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("set playlist", () =>
{
SelectedRoom.Value!.Playlist =
room.Playlist =
[
new PlaylistItem(new BeatmapInfo { StarRating = min }),
new PlaylistItem(new BeatmapInfo { StarRating = max }),
new PlaylistItem(new BeatmapInfo { StarRating = min }) { ID = TestResources.GetNextTestID() },
new PlaylistItem(new BeatmapInfo { StarRating = max }) { ID = TestResources.GetNextTestID() },
];
});
}

View File

@ -355,18 +355,18 @@ namespace osu.Game.Tests.Visual.Navigation
}
[Test]
public void TestLastScoreNullAfterExitingPlayer()
public void TestLastScoreNotNullAfterExitingPlayer()
{
AddUntilStep("wait for last play null", getLastPlay, () => Is.Null);
AddUntilStep("last play null", getLastPlay, () => Is.Null);
var getOriginalPlayer = playToCompletion();
AddStep("attempt to retry", () => getOriginalPlayer().ChildrenOfType<HotkeyRetryOverlay>().First().Action());
AddUntilStep("wait for last play matches player", getLastPlay, () => Is.EqualTo(getOriginalPlayer().Score.ScoreInfo));
AddUntilStep("last play matches player", getLastPlay, () => Is.EqualTo(getOriginalPlayer().Score.ScoreInfo));
AddUntilStep("wait for player", () => Game.ScreenStack.CurrentScreen != getOriginalPlayer() && Game.ScreenStack.CurrentScreen is Player);
AddStep("exit player", () => (Game.ScreenStack.CurrentScreen as Player)?.Exit());
AddUntilStep("wait for last play null", getLastPlay, () => Is.Null);
AddUntilStep("last play not null", getLastPlay, () => Is.Not.Null);
ScoreInfo getLastPlay() => Game.Dependencies.Get<SessionStatics>().Get<ScoreInfo>(Static.LastLocalUserScore);
}

View File

@ -12,7 +12,6 @@ using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
@ -85,6 +84,7 @@ namespace osu.Game.Tests.Visual.SongSelect
}
[Test]
[FlakyTest]
public void TestPresentedBeatmapIsRecommended()
{
List<BeatmapSetInfo> beatmapSets = null;
@ -106,6 +106,7 @@ namespace osu.Game.Tests.Visual.SongSelect
}
[Test]
[FlakyTest]
public void TestCurrentRulesetIsRecommended()
{
BeatmapSetInfo catchSet = null, mixedSet = null;
@ -142,6 +143,7 @@ namespace osu.Game.Tests.Visual.SongSelect
}
[Test]
[FlakyTest]
public void TestSecondBestRulesetIsRecommended()
{
BeatmapSetInfo osuSet = null, mixedSet = null;
@ -159,6 +161,7 @@ namespace osu.Game.Tests.Visual.SongSelect
}
[Test]
[FlakyTest]
public void TestCorrectStarRatingIsUsed()
{
BeatmapSetInfo osuSet = null, maniaSet = null;
@ -176,6 +179,7 @@ namespace osu.Game.Tests.Visual.SongSelect
}
[Test]
[FlakyTest]
public void TestBeatmapListingFilter()
{
AddStep("set playmode to taiko", () => ((DummyAPIAccess)API).LocalUser.Value.PlayMode = "taiko");
@ -245,7 +249,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("present beatmap", () => Game.PresentBeatmap(getImport()));
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect select && select.BeatmapSetsLoaded);
AddUntilStep("recommended beatmap displayed", () => Game.Beatmap.Value.BeatmapInfo.MatchesOnlineID(getImport().Beatmaps[expectedDiff - 1]));
AddUntilStep("recommended beatmap displayed", () => Game.Beatmap.Value.BeatmapInfo.OnlineID, () => Is.EqualTo(getImport().Beatmaps[expectedDiff - 1].OnlineID));
}
protected override TestOsuGame CreateTestGame() => new NoBeatmapUpdateGame(LocalStorage, API);

View File

@ -1,11 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="DeepEqual" Version="4.2.1" />
<PackageReference Include="Nito.AsyncEx" Version="5.1.2" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
<PackageReference Include="Moq" Version="4.18.4" />
</ItemGroup>
<PropertyGroup Label="Project">

View File

@ -4,9 +4,9 @@
<StartupObject>osu.Game.Tournament.Tests.TournamentTestRunner</StartupObject>
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
</ItemGroup>
<PropertyGroup Label="Project">
<OutputType>WinExe</OutputType>

View File

@ -33,12 +33,12 @@ namespace osu.Game.Audio
/// <summary>
/// All valid sample addition constants.
/// </summary>
public static IEnumerable<string> AllAdditions => new[] { HIT_WHISTLE, HIT_FINISH, HIT_CLAP };
public static readonly string[] ALL_ADDITIONS = [HIT_WHISTLE, HIT_FINISH, HIT_CLAP];
/// <summary>
/// All valid bank constants.
/// </summary>
public static IEnumerable<string> AllBanks => new[] { BANK_NORMAL, BANK_SOFT, BANK_DRUM };
public static readonly string[] ALL_BANKS = [BANK_NORMAL, BANK_SOFT, BANK_DRUM];
/// <summary>
/// The name of the sample to load.

View File

@ -15,7 +15,7 @@ namespace osu.Game.Beatmaps
/// Returns <see langword="true"/> if the character <paramref name="c"/> can be used in <see cref="BeatmapMetadata.Artist"/> and <see cref="BeatmapMetadata.Title"/> fields.
/// Characters not matched by this method can be placed in <see cref="BeatmapMetadata.ArtistUnicode"/> and <see cref="BeatmapMetadata.TitleUnicode"/>.
/// </summary>
public static bool IsRomanised(char c) => c <= 0xFF;
public static bool IsRomanised(char c) => char.IsAscii(c) && !char.IsControl(c);
/// <summary>
/// Returns <see langword="true"/> if the string <paramref name="str"/> can be used in <see cref="BeatmapMetadata.Artist"/> and <see cref="BeatmapMetadata.Title"/> fields.

View File

@ -3,6 +3,7 @@
using System;
using System.Diagnostics;
using osu.Framework;
using osu.Framework.Bindables;
using osu.Framework.Configuration;
using osu.Framework.Configuration.Tracking;
@ -163,6 +164,7 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.Version, string.Empty);
SetDefault(OsuSetting.ShowFirstRunSetup, true);
SetDefault(OsuSetting.ShowMobileDisclaimer, RuntimeInfo.IsMobile);
SetDefault(OsuSetting.ScreenshotFormat, ScreenshotFormat.Jpg);
SetDefault(OsuSetting.ScreenshotCaptureMenuCursor, false);
@ -453,6 +455,7 @@ namespace osu.Game.Configuration
AlwaysRequireHoldingForPause,
MultiplayerShowInProgressFilter,
BeatmapListingFeaturedArtistFilter,
ShowMobileDisclaimer,
EditorShowStoryboard,
}
}

View File

@ -10,7 +10,6 @@ using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
namespace osu.Game.Configuration
{
@ -30,6 +29,7 @@ namespace osu.Game.Configuration
SetDefault<APISeasonalBackgrounds>(Static.SeasonalBackgrounds, null);
SetDefault(Static.TouchInputActive, RuntimeInfo.IsMobile);
SetDefault<ScoreInfo>(Static.LastLocalUserScore, null);
SetDefault<ScoreInfo>(Static.LastAppliedOffsetScore, null);
}
/// <summary>
@ -78,11 +78,15 @@ namespace osu.Game.Configuration
TouchInputActive,
/// <summary>
/// Contains the local user's last score (can be completed or aborted) after exiting <see cref="Player"/>.
/// Will be cleared to <c>null</c> when leaving <see cref="PlayerLoader"/>.
/// Stores the local user's last score (can be completed or aborted).
/// </summary>
LastLocalUserScore,
/// <summary>
/// Stores the local user's last score which was used to apply an offset.
/// </summary>
LastAppliedOffsetScore,
/// <summary>
/// Whether the intro animation for the daily challenge screen has been played once.
/// This is reset when a new challenge is up.

View File

@ -42,7 +42,10 @@ namespace osu.Game.Database
return null;
using var contentStreamReader = new LineBufferedReader(contentStream);
var beatmapContent = new LegacyBeatmapDecoder().Decode(contentStreamReader);
// FIRST_LAZER_VERSION is specified here to avoid flooring object coordinates on decode via `(int)` casts.
// we will be making integers out of them lower down, but in a slightly different manner (rounding rather than truncating)
var beatmapContent = new LegacyBeatmapDecoder(LegacyBeatmapEncoder.FIRST_LAZER_VERSION).Decode(contentStreamReader);
var workingBeatmap = new FlatWorkingBeatmap(beatmapContent);
var playableBeatmap = workingBeatmap.GetPlayableBeatmap(beatmapInfo.Ruleset);
@ -93,6 +96,12 @@ namespace osu.Game.Database
hitObject.StartTime = Math.Floor(hitObject.StartTime);
if (hitObject is IHasXPosition hasXPosition)
hasXPosition.X = MathF.Round(hasXPosition.X);
if (hitObject is IHasYPosition hasYPosition)
hasYPosition.Y = MathF.Round(hasYPosition.Y);
if (hitObject is not IHasPath hasPath) continue;
// stable's hit object parsing expects the entire slider to use only one type of curve,

View File

@ -266,7 +266,7 @@ namespace osu.Game.Database
/// <para>
/// If a write transaction did not modify any objects in this <see cref="IRealmCollection{T}" />, the callback is not invoked at all.
/// If an error occurs the callback will be invoked with <c>null</c> for the <c>sender</c> parameter and a non-<c>null</c> <c>error</c>.
/// Currently the only errors that can occur are when opening the <see cref="Realm" /> on the background worker thread.
/// Currently, the only errors that can occur are when opening the <see cref="Realm" /> on the background worker thread.
/// </para>
/// <para>
/// At the time when the block is called, the <see cref="IRealmCollection{T}" /> object will be fully evaluated
@ -285,8 +285,8 @@ namespace osu.Game.Database
/// A subscription token. It must be kept alive for as long as you want to receive change notifications.
/// To stop receiving notifications, call <see cref="IDisposable.Dispose" />.
/// </returns>
/// <seealso cref="Realms.CollectionExtensions.SubscribeForNotifications{T}(IList{T}, NotificationCallbackDelegate{T})" />
/// <seealso cref="Realms.CollectionExtensions.SubscribeForNotifications{T}(IQueryable{T}, NotificationCallbackDelegate{T})" />
/// <seealso cref="Realms.CollectionExtensions.SubscribeForNotifications{T}(IList{T}, NotificationCallbackDelegate{T},KeyPathsCollection?)" />
/// <seealso cref="Realms.CollectionExtensions.SubscribeForNotifications{T}(IQueryable{T}, NotificationCallbackDelegate{T},KeyPathsCollection?)" />
#pragma warning restore RS0030
public static IDisposable QueryAsyncWithNotifications<T>(this IRealmCollection<T> collection, NotificationCallbackDelegate<T> callback)
where T : RealmObjectBase

View File

@ -46,7 +46,8 @@ namespace osu.Game.Database
}
public IRealmCollection<T> Freeze() => throw new NotImplementedException();
public IDisposable SubscribeForNotifications(NotificationCallbackDelegate<T> callback) => throw new NotImplementedException();
public IDisposable SubscribeForNotifications(NotificationCallbackDelegate<T> callback, KeyPathsCollection? keyPathCollection = null) => throw new NotImplementedException();
public bool IsValid => throw new NotImplementedException();
public Realm Realm => throw new NotImplementedException();
public ObjectSchema ObjectSchema => throw new NotImplementedException();

View File

@ -59,6 +59,25 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString DailyChallenge => new TranslatableString(getKey(@"daily_challenge"), @"daily challenge");
/// <summary>
/// "A few important words from your dev team!"
/// </summary>
public static LocalisableString MobileDisclaimerHeader => new TranslatableString(getKey(@"mobile_disclaimer_header"), @"A few important words from your dev team!");
/// <summary>
/// "While we have released osu! on mobile platforms to maximise the number of people that can enjoy the game, our focus is still on the PC version.
///
/// Your experience will not be perfect, and may even feel subpar compared to games which are made mobile-first.
///
/// Please bear with us as we continue to improve the game for you!"
/// </summary>
public static LocalisableString MobileDisclaimerBody => new TranslatableString(getKey(@"mobile_disclaimer_body"),
@"While we have released osu! on mobile platforms to maximise the number of people that can enjoy the game, our focus is still on the PC version.
Your experience will not be perfect, and may even feel subpar compared to games which are made mobile-first.
Please bear with us as we continue to improve the game for you!");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -15,10 +15,9 @@ 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 not affect your existing installation's files in any way."
/// "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&#39;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 not affect your existing installation's files in any way.");
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 not affect your existing installation's files in any way.");
/// <summary>
/// "previous osu! install"
@ -38,8 +37,7 @@ namespace osu.Game.Localisation
/// <summary>
/// "Your import will continue in the background. Check on its progress in the notifications sidebar!"
/// </summary>
public static LocalisableString ImportInProgress =>
new TranslatableString(getKey(@"import_in_progress"), @"Your import will continue in the background. Check on its progress in the notifications sidebar!");
public static LocalisableString ImportInProgress => new TranslatableString(getKey(@"import_in_progress"), @"Your import will continue in the background. Check on its progress in the notifications sidebar!");
/// <summary>
/// "calculating..."

View File

@ -84,12 +84,12 @@ Please try changing your audio device to a working setting.");
public static LocalisableString LinkTypeNotSupported => new TranslatableString(getKey(@"unsupported_link_type"), @"This link type is not yet supported!");
/// <summary>
/// "You received a private message from '{0}'. Click to read it!"
/// "You received a private message from &#39;{0}&#39;. Click to read it!"
/// </summary>
public static LocalisableString PrivateMessageReceived(string username) => new TranslatableString(getKey(@"private_message_received"), @"You received a private message from '{0}'. Click to read it!", username);
/// <summary>
/// "Your name was mentioned in chat by '{0}'. Click to find out why!"
/// "Your name was mentioned in chat by &#39;{0}&#39;. Click to find out why!"
/// </summary>
public static LocalisableString YourNameWasMentioned(string username) => new TranslatableString(getKey(@"your_name_was_mentioned"), @"Your name was mentioned in chat by '{0}'. Click to find out why!", username);
@ -115,7 +115,7 @@ Please try changing your audio device to a working setting.");
/// <summary>
/// "You are now running osu! {0}.
/// Click to see what's new!"
/// Click to see what&#39;s new!"
/// </summary>
public static LocalisableString GameVersionAfterUpdate(string version) => new TranslatableString(getKey(@"game_version_after_update"), @"You are now running osu! {0}.
Click to see what's new!", version);

View File

@ -10,10 +10,12 @@ using osu.Game.Configuration;
namespace osu.Game.Online.API
{
public class ModSettingsDictionaryFormatter : IMessagePackFormatter<Dictionary<string, object>>
public class ModSettingsDictionaryFormatter : IMessagePackFormatter<Dictionary<string, object>?>
{
public void Serialize(ref MessagePackWriter writer, Dictionary<string, object> value, MessagePackSerializerOptions options)
public void Serialize(ref MessagePackWriter writer, Dictionary<string, object>? value, MessagePackSerializerOptions options)
{
if (value == null) return;
var primitiveFormatter = PrimitiveObjectFormatter.Instance;
writer.WriteArrayHeader(value.Count);

View File

@ -4,13 +4,16 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Platform;
using osu.Game.Configuration;
using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Overlays;
using osu.Game.Overlays.Dialog;
using osu.Game.Overlays.Notifications;
using WebCommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings;
namespace osu.Game.Online.Chat
@ -23,9 +26,15 @@ namespace osu.Game.Online.Chat
[Resolved]
private Clipboard clipboard { get; set; } = null!;
[Resolved(CanBeNull = true)]
[Resolved]
private IDialogOverlay? dialogOverlay { get; set; }
[Resolved]
private INotificationOverlay? notificationOverlay { get; set; }
[Resolved]
private IAPIProvider api { get; set; } = null!;
private Bindable<bool> externalLinkWarning = null!;
[BackgroundDependencyLoader(true)]
@ -34,9 +43,51 @@ namespace osu.Game.Online.Chat
externalLinkWarning = config.GetBindable<bool>(OsuSetting.ExternalLinkWarning);
}
public void OpenUrlExternally(string url, bool bypassWarning = false)
public void OpenUrlExternally(string url, LinkWarnMode warnMode = LinkWarnMode.Default)
{
if (!bypassWarning && externalLinkWarning.Value && dialogOverlay != null)
bool isTrustedDomain;
if (url.StartsWith('/'))
{
url = $"{api.WebsiteRootUrl}{url}";
isTrustedDomain = true;
}
else
{
isTrustedDomain = url.StartsWith(api.WebsiteRootUrl, StringComparison.Ordinal);
}
if (!url.CheckIsValidUrl())
{
notificationOverlay?.Post(new SimpleErrorNotification
{
Text = NotificationsStrings.UnsupportedOrDangerousUrlProtocol(url),
});
return;
}
bool shouldWarn;
switch (warnMode)
{
case LinkWarnMode.Default:
shouldWarn = externalLinkWarning.Value && !isTrustedDomain;
break;
case LinkWarnMode.AlwaysWarn:
shouldWarn = true;
break;
case LinkWarnMode.NeverWarn:
shouldWarn = false;
break;
default:
throw new ArgumentOutOfRangeException(nameof(warnMode), warnMode, null);
}
if (dialogOverlay != null && shouldWarn)
dialogOverlay.Push(new ExternalLinkDialog(url, () => host.OpenUrlExternally(url), () => clipboard.SetText(url)));
else
host.OpenUrlExternally(url);

View File

@ -0,0 +1,23 @@
// 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.Online.Chat
{
public enum LinkWarnMode
{
/// <summary>
/// Will show a dialog when opening a URL that is not on a trusted domain.
/// </summary>
Default,
/// <summary>
/// Will always show a dialog when opening a URL.
/// </summary>
AlwaysWarn,
/// <summary>
/// Will never show a dialog when opening a URL.
/// </summary>
NeverWarn,
}
}

View File

@ -5,6 +5,7 @@ using MessagePack;
namespace osu.Game.Online.Multiplayer.MatchTypes.TeamVersus
{
[MessagePackObject]
public class TeamVersusUserState : MatchUserState
{
[Key(0)]

View File

@ -18,7 +18,6 @@ using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Configuration;
using osu.Framework.Extensions;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics;
@ -516,32 +515,7 @@ namespace osu.Game
onScreenDisplay.Display(new CopyUrlToast());
});
public void OpenUrlExternally(string url, bool forceBypassExternalUrlWarning = false) => waitForReady(() => externalLinkOpener, _ =>
{
bool isTrustedDomain;
if (url.StartsWith('/'))
{
url = $"{API.WebsiteRootUrl}{url}";
isTrustedDomain = true;
}
else
{
isTrustedDomain = url.StartsWith(API.WebsiteRootUrl, StringComparison.Ordinal);
}
if (!url.CheckIsValidUrl())
{
Notifications.Post(new SimpleErrorNotification
{
Text = NotificationsStrings.UnsupportedOrDangerousUrlProtocol(url),
});
return;
}
externalLinkOpener.OpenUrlExternally(url, forceBypassExternalUrlWarning || isTrustedDomain);
});
public void OpenUrlExternally(string url, LinkWarnMode warnMode = LinkWarnMode.Default) => waitForReady(() => externalLinkOpener, _ => externalLinkOpener.OpenUrlExternally(url, warnMode));
/// <summary>
/// Open a specific channel in chat.
@ -1340,7 +1314,7 @@ namespace osu.Game
IconColour = Colours.YellowDark,
Activated = () =>
{
OpenUrlExternally("https://opentabletdriver.net/Tablets", true);
OpenUrlExternally("https://opentabletdriver.net/Tablets", LinkWarnMode.NeverWarn);
return true;
}
}));

View File

@ -315,6 +315,7 @@ namespace osu.Game
dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, realm, API, LocalConfig));
dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, realm, API, Audio, Resources, Host, defaultBeatmap, difficultyCache, performOnlineLookups: true));
dependencies.CacheAs<IWorkingBeatmapCache>(BeatmapManager);
dependencies.Cache(BeatmapDownloader = new BeatmapModelDownloader(BeatmapManager, API));
dependencies.Cache(ScoreDownloader = new ScoreModelDownloader(ScoreManager, API));

View File

@ -18,6 +18,7 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Online.Chat;
using osu.Game.Overlays.Settings;
using osu.Game.Resources.Localisation.Web;
using osuTK;
@ -213,7 +214,7 @@ namespace osu.Game.Overlays.AccountCreation
if (!string.IsNullOrEmpty(errors.Message))
passwordDescription.AddErrors(new[] { errors.Message });
game?.OpenUrlExternally($"{errors.Redirect}?username={usernameTextBox.Text}&email={emailTextBox.Text}", true);
game?.OpenUrlExternally($"{errors.Redirect}?username={usernameTextBox.Text}&email={emailTextBox.Text}", LinkWarnMode.NeverWarn);
}
}
else

View File

@ -75,7 +75,9 @@ namespace osu.Game.Overlays.Dialog
return;
bodyText = value;
body.Text = value;
body.TextAnchor = bodyText.ToString().Contains('\n') ? Anchor.TopLeft : Anchor.TopCentre;
}
}
@ -210,13 +212,12 @@ namespace osu.Game.Overlays.Dialog
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
TextAnchor = Anchor.TopCentre,
Padding = new MarginPadding { Horizontal = 15 },
Padding = new MarginPadding { Horizontal = 15, Bottom = 10 },
},
body = new OsuTextFlowContainer(t => t.Font = t.Font.With(size: 18))
{
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
TextAnchor = Anchor.TopCentre,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Horizontal = 15 },
@ -301,6 +302,7 @@ namespace osu.Game.Overlays.Dialog
{
content.ScaleTo(0.7f);
ring.ResizeTo(ringMinifiedSize);
icon.ScaleTo(0f);
}
content
@ -308,6 +310,7 @@ namespace osu.Game.Overlays.Dialog
.FadeIn(ENTER_DURATION, Easing.OutQuint);
ring.ResizeTo(ringSize, ENTER_DURATION * 1.5f, Easing.OutQuint);
icon.Delay(100).ScaleTo(1, ENTER_DURATION * 1.5f, Easing.OutQuint);
}
protected override void PopOut()

View File

@ -121,9 +121,11 @@ namespace osu.Game.Overlays.Login
codeTextBox.Current.BindValueChanged(code =>
{
if (code.NewValue.Length == 8)
string trimmedCode = code.NewValue.Trim();
if (trimmedCode.Length == 8)
{
api.AuthenticateSecondFactor(code.NewValue);
api.AuthenticateSecondFactor(trimmedCode);
codeTextBox.Current.Disabled = true;
}
});

View File

@ -11,6 +11,7 @@ using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Online.Chat;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.Profile.Header.Components
@ -87,7 +88,8 @@ namespace osu.Game.Overlays.Profile.Header.Components
{
background.Colour = colours.Pink;
Action = () => game?.OpenUrlExternally(@"/home/support");
// Easy to accidentally click so let's always show the open URL popup.
Action = () => game?.OpenUrlExternally(@"/home/support", LinkWarnMode.AlwaysWarn);
}
protected override bool OnHover(HoverEvent e)

View File

@ -119,8 +119,8 @@ namespace osu.Game.Rulesets.Edit.Checks
string bank = parts[0];
string sampleSet = parts[1];
return HitSampleInfo.AllBanks.Contains(bank)
&& HitSampleInfo.AllAdditions.Append(HitSampleInfo.HIT_NORMAL).Any(sampleSet.StartsWith);
return HitSampleInfo.ALL_BANKS.Contains(bank)
&& HitSampleInfo.ALL_ADDITIONS.Append(HitSampleInfo.HIT_NORMAL).Any(sampleSet.StartsWith);
}
public class IssueTemplateConsequentDelay : IssueTemplate

View File

@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Edit.Checks
++objectsWithoutHitsounds;
}
private bool isHitsound(HitSampleInfo sample) => HitSampleInfo.AllAdditions.Any(sample.Name.Contains);
private bool isHitsound(HitSampleInfo sample) => HitSampleInfo.ALL_ADDITIONS.Any(sample.Name.Contains);
private bool isHitnormal(HitSampleInfo sample) => sample.Name.Contains(HitSampleInfo.HIT_NORMAL);
public abstract class IssueTemplateLongPeriod : IssueTemplate

View File

@ -55,12 +55,6 @@ namespace osu.Game.Rulesets.Edit
}
}
protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => base.ReceivePositionalInputAtSubTree(screenSpacePos) && anyToolboxHovered(screenSpacePos);
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) && anyToolboxHovered(screenSpacePos);
private bool anyToolboxHovered(Vector2 screenSpacePos) => FillFlow.ScreenSpaceDrawQuad.Contains(screenSpacePos);
protected override bool OnMouseDown(MouseDownEvent e) => true;
protected override bool OnClick(ClickEvent e) => true;

View File

@ -21,9 +21,17 @@ namespace osu.Game.Rulesets.Objects.Legacy
public int ComboOffset { get; set; }
public float X => Position.X;
public float X
{
get => Position.X;
set => Position = new Vector2(value, Position.Y);
}
public float Y => Position.Y;
public float Y
{
get => Position.Y;
set => Position = new Vector2(Position.X, value);
}
public Vector2 Position { get; set; }

View File

@ -44,13 +44,13 @@ namespace osu.Game.Rulesets.Objects
PathProgress = 0,
};
if (tickDistance != 0)
for (int span = 0; span < spanCount; span++)
{
for (int span = 0; span < spanCount; span++)
{
double spanStartTime = startTime + span * spanDuration;
bool reversed = span % 2 == 1;
double spanStartTime = startTime + span * spanDuration;
bool reversed = span % 2 == 1;
if (tickDistance != 0)
{
var ticks = generateTicks(span, spanStartTime, spanDuration, reversed, length, tickDistance, minDistanceFromEnd, cancellationToken);
if (reversed)
@ -61,18 +61,18 @@ namespace osu.Game.Rulesets.Objects
foreach (var e in ticks)
yield return e;
}
if (span < spanCount - 1)
if (span < spanCount - 1)
{
yield return new SliderEventDescriptor
{
yield return new SliderEventDescriptor
{
Type = SliderEventType.Repeat,
SpanIndex = span,
SpanStartTime = startTime + span * spanDuration,
Time = spanStartTime + spanDuration,
PathProgress = (span + 1) % 2,
};
}
Type = SliderEventType.Repeat,
SpanIndex = span,
SpanStartTime = startTime + span * spanDuration,
Time = spanStartTime + spanDuration,
PathProgress = (span + 1) % 2,
};
}
}

View File

@ -13,6 +13,6 @@ namespace osu.Game.Rulesets.Objects.Types
/// <summary>
/// The starting position of the HitObject.
/// </summary>
Vector2 Position { get; }
Vector2 Position { get; set; }
}
}

View File

@ -11,6 +11,6 @@ namespace osu.Game.Rulesets.Objects.Types
/// <summary>
/// The starting X-position of this HitObject.
/// </summary>
float X { get; }
float X { get; set; }
}
}

View File

@ -11,6 +11,6 @@ namespace osu.Game.Rulesets.Objects.Types
/// <summary>
/// The starting Y-position of this HitObject.
/// </summary>
float Y { get; }
float Y { get; set; }
}
}

View File

@ -79,7 +79,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// </summary>
private void createStateBindables()
{
foreach (string bankName in HitSampleInfo.AllBanks.Prepend(HIT_BANK_AUTO))
foreach (string bankName in HitSampleInfo.ALL_BANKS.Prepend(HIT_BANK_AUTO))
{
var bindable = new Bindable<TernaryState>
{
@ -143,7 +143,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
SelectionBankStates[bankName] = bindable;
}
foreach (string bankName in HitSampleInfo.AllBanks.Prepend(HIT_BANK_AUTO))
foreach (string bankName in HitSampleInfo.ALL_BANKS.Prepend(HIT_BANK_AUTO))
{
var bindable = new Bindable<TernaryState>
{
@ -216,7 +216,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
resetTernaryStates();
foreach (string sampleName in HitSampleInfo.AllAdditions)
foreach (string sampleName in HitSampleInfo.ALL_ADDITIONS)
{
var bindable = new Bindable<TernaryState>
{

View File

@ -409,7 +409,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private void createStateBindables()
{
foreach (string sampleName in HitSampleInfo.AllAdditions)
foreach (string sampleName in HitSampleInfo.ALL_ADDITIONS)
{
var bindable = new Bindable<TernaryState>
{
@ -433,7 +433,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
selectionSampleStates[sampleName] = bindable;
}
banks.AddRange(HitSampleInfo.AllBanks.Prepend(EditorSelectionHandler.HIT_BANK_AUTO));
banks.AddRange(HitSampleInfo.ALL_BANKS.Prepend(EditorSelectionHandler.HIT_BANK_AUTO));
}
private void updateTernaryStates()

View File

@ -155,9 +155,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
if (hitObject.GetEndTime() < editorClock.CurrentTime - timeline.VisibleRange / 2)
break;
foreach (var sample in hitObject.Samples)
for (int i = 0; i < hitObject.Samples.Count; i++)
{
if (!HitSampleInfo.AllBanks.Contains(sample.Bank))
var sample = hitObject.Samples[i];
if (!HitSampleInfo.ALL_BANKS.Contains(sample.Bank))
minimumGap = Math.Max(minimumGap, absolute_minimum_gap + sample.Bank.Length * 3);
}
@ -165,10 +167,17 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
smallestTimeGap = Math.Min(smallestTimeGap, hasRepeats.Duration / hasRepeats.SpanCount() / 2);
foreach (var sample in hasRepeats.NodeSamples.SelectMany(s => s))
for (int i = 0; i < hasRepeats.NodeSamples.Count; i++)
{
if (!HitSampleInfo.AllBanks.Contains(sample.Bank))
minimumGap = Math.Max(minimumGap, absolute_minimum_gap + sample.Bank.Length * 3);
var node = hasRepeats.NodeSamples[i];
for (int j = 0; j < node.Count; j++)
{
var sample = node[j];
if (!HitSampleInfo.ALL_BANKS.Contains(sample.Bank))
minimumGap = Math.Max(minimumGap, absolute_minimum_gap + sample.Bank.Length * 3);
}
}
}

View File

@ -12,6 +12,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Threading;
using osu.Framework.Timing;
using osu.Framework.Utils;
@ -41,6 +42,9 @@ namespace osu.Game.Screens.Edit.Timing
[Resolved]
private OverlayColourProvider overlayColourProvider { get; set; } = null!;
[Resolved]
private BindableBeatDivisor beatDivisor { get; set; } = null!;
public bool EnableClicking
{
get => metronomeTick.EnableClicking;
@ -222,7 +226,7 @@ namespace osu.Game.Screens.Edit.Timing
Clock = new FramedClock(metronomeClock = new StopwatchClock(true));
}
private double beatLength;
private double effectiveBeatLength;
private TimingControlPoint timingPoint = null!;
@ -232,11 +236,26 @@ namespace osu.Game.Screens.Edit.Timing
private ScheduledDelegate? latchDelegate;
private bool spedUp;
private int computeSpedUpDivisor()
{
if (!spedUp)
return 1;
if (beatDivisor.Value % 3 == 0)
return 3;
if (beatDivisor.Value % 2 == 0)
return 2;
return 1;
}
protected override void LoadComplete()
{
base.LoadComplete();
interpolatedBpm.BindValueChanged(bpm => bpmText.Text = bpm.NewValue.ToLocalisableString());
interpolatedBpm.BindValueChanged(_ => bpmText.Text = interpolatedBpm.Value.ToLocalisableString());
}
protected override void Update()
@ -250,16 +269,20 @@ namespace osu.Game.Screens.Edit.Timing
timingPoint = BeatSyncSource.ControlPoints.TimingPointAt(BeatSyncSource.Clock.CurrentTime);
if (beatLength != timingPoint.BeatLength)
Divisor = metronomeTick.Divisor = computeSpedUpDivisor();
if (effectiveBeatLength != timingPoint.BeatLength / Divisor)
{
beatLength = timingPoint.BeatLength;
effectiveBeatLength = timingPoint.BeatLength / Divisor;
EarlyActivationMilliseconds = timingPoint.BeatLength / 2;
float bpmRatio = (float)Interpolation.ApplyEasing(Easing.OutQuad, Math.Clamp((timingPoint.BPM - 30) / 480, 0, 1));
double effectiveBpm = 60000 / effectiveBeatLength;
float bpmRatio = (float)Interpolation.ApplyEasing(Easing.OutQuad, Math.Clamp((effectiveBpm - 30) / 480, 0, 1));
weight.MoveToY((float)Interpolation.Lerp(0.1f, 0.83f, bpmRatio), 600, Easing.OutQuint);
this.TransformBindableTo(interpolatedBpm, (int)Math.Round(timingPoint.BPM), 600, Easing.OutQuint);
this.TransformBindableTo(interpolatedBpm, (int)Math.Round(effectiveBpm), 600, Easing.OutQuint);
}
if (!BeatSyncSource.Clock.IsRunning && isSwinging)
@ -305,7 +328,7 @@ namespace osu.Game.Screens.Edit.Timing
float currentAngle = swing.Rotation;
float targetAngle = currentAngle > 0 ? -angle : angle;
swing.RotateTo(targetAngle, beatLength, Easing.InOutQuad);
swing.RotateTo(targetAngle, effectiveBeatLength, Easing.InOutQuad);
}
private void onTickPlayed()
@ -313,9 +336,25 @@ namespace osu.Game.Screens.Edit.Timing
// Originally, this flash only occurred when the pendulum correctly passess the centre.
// Mappers weren't happy with the metronome tick not playing immediately after starting playback
// so now this matches the actual tick sample.
stick.FlashColour(overlayColourProvider.Content1, beatLength, Easing.OutQuint);
stick.FlashColour(overlayColourProvider.Content1, effectiveBeatLength, Easing.OutQuint);
}
protected override bool OnKeyDown(KeyDownEvent e)
{
updateDivisorFromKey(e);
return base.OnKeyDown(e);
}
protected override void OnKeyUp(KeyUpEvent e)
{
base.OnKeyUp(e);
updateDivisorFromKey(e);
}
private void updateDivisorFromKey(UIEvent e) => spedUp = e.ControlPressed;
private partial class MetronomeTick : BeatSyncedContainer
{
public bool EnableClicking;

View File

@ -20,6 +20,7 @@ using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Overlays;
@ -170,7 +171,14 @@ namespace osu.Game.Screens.Menu
if (s.Beatmaps.Count == 0)
return;
initialBeatmap = beatmaps.GetWorkingBeatmap(s.Beatmaps.First());
var working = beatmaps.GetWorkingBeatmap(s.Beatmaps.First());
// Ensure files area actually present on disk.
// This is to handle edge cases like users deleting files outside the game and breaking the world.
if (!hasAllFiles(working))
return;
initialBeatmap = working;
});
return UsingThemedIntro = initialBeatmap != null;
@ -188,6 +196,20 @@ namespace osu.Game.Screens.Menu
[Resolved]
private INotificationOverlay notifications { get; set; }
private bool hasAllFiles(WorkingBeatmap working)
{
foreach (var f in working.BeatmapSetInfo.Files)
{
using (var str = working.GetStream(f.File.GetStoragePath()))
{
if (str == null)
return false;
}
}
return true;
}
private void ensureEventuallyArrivingAtMenu()
{
// This intends to handle the case where an intro may get stuck.

View File

@ -13,11 +13,13 @@ using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics;
@ -39,6 +41,7 @@ using osu.Game.Screens.Select;
using osu.Game.Seasonal;
using osuTK;
using osuTK.Graphics;
using osu.Game.Localisation;
namespace osu.Game.Screens.Menu
{
@ -87,6 +90,7 @@ namespace osu.Game.Screens.Menu
private Bindable<double> holdDelay;
private Bindable<bool> loginDisplayed;
private Bindable<bool> showMobileDisclaimer;
private HoldToExitGameOverlay holdToExitGameOverlay;
@ -111,6 +115,7 @@ namespace osu.Game.Screens.Menu
{
holdDelay = config.GetBindable<double>(OsuSetting.UIHoldActivationDelay);
loginDisplayed = statics.GetBindable<bool>(Static.LoginOverlayDisplayed);
showMobileDisclaimer = config.GetBindable<bool>(OsuSetting.ShowMobileDisclaimer);
if (host.CanExit)
{
@ -255,6 +260,9 @@ namespace osu.Game.Screens.Menu
[CanBeNull]
private Drawable proxiedLogo;
[CanBeNull]
private ScheduledDelegate mobileDisclaimerSchedule;
protected override void LogoArriving(OsuLogo logo, bool resuming)
{
base.LogoArriving(logo, resuming);
@ -275,26 +283,46 @@ namespace osu.Game.Screens.Menu
sideFlashes.Delay(FADE_IN_DURATION).FadeIn(64, Easing.InQuint);
}
else if (!api.IsLoggedIn || api.State.Value == APIState.RequiresSecondFactorAuth)
else
{
// copy out old action to avoid accidentally capturing logo.Action in closure, causing a self-reference loop.
var previousAction = logo.Action;
// we want to hook into logo.Action to display the login overlay, but also preserve the return value of the old action.
// we want to hook into logo.Action to display certain overlays, but also preserve the return value of the old action.
// therefore pass the old action to displayLogin, so that it can return that value.
// this ensures that the OsuLogo sample does not play when it is not desired.
logo.Action = () => displayLogin(previousAction);
logo.Action = () => onLogoClick(previousAction);
}
}
bool displayLogin(Func<bool> originalAction)
private bool onLogoClick(Func<bool> originalAction)
{
if (showMobileDisclaimer.Value)
{
if (!loginDisplayed.Value)
mobileDisclaimerSchedule?.Cancel();
mobileDisclaimerSchedule = Scheduler.AddDelayed(() =>
{
Scheduler.AddDelayed(() => login?.Show(), 500);
loginDisplayed.Value = true;
}
dialogOverlay.Push(new MobileDisclaimerDialog(() =>
{
showMobileDisclaimer.Value = false;
displayLoginIfApplicable();
}));
}, 500);
}
else
displayLoginIfApplicable();
return originalAction.Invoke();
return originalAction.Invoke();
}
private void displayLoginIfApplicable()
{
if (loginDisplayed.Value) return;
if (!api.IsLoggedIn || api.State.Value == APIState.RequiresSecondFactorAuth)
{
Scheduler.AddDelayed(() => login?.Show(), 500);
loginDisplayed.Value = true;
}
}
@ -443,5 +471,25 @@ namespace osu.Game.Screens.Menu
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
{
}
private partial class MobileDisclaimerDialog : PopupDialog
{
public MobileDisclaimerDialog(Action confirmed)
{
HeaderText = ButtonSystemStrings.MobileDisclaimerHeader;
BodyText = ButtonSystemStrings.MobileDisclaimerBody;
Icon = FontAwesome.Solid.SmileBeam;
Buttons = new PopupDialogButton[]
{
new PopupDialogOkButton
{
Text = "Understood",
Action = confirmed,
},
};
}
}
}
}

View File

@ -14,7 +14,6 @@ using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics;
using osu.Game.Online.Rooms;
using osuTK;
using Container = osu.Framework.Graphics.Containers.Container;
namespace osu.Game.Screens.OnlinePlay.Components
{
@ -30,6 +29,8 @@ namespace osu.Game.Screens.OnlinePlay.Components
private StarRatingDisplay maxDisplay = null!;
private Drawable maxBackground = null!;
private BufferedContainer bufferedContent = null!;
public StarRatingRangeDisplay(Room room)
{
this.room = room;
@ -41,38 +42,43 @@ namespace osu.Game.Screens.OnlinePlay.Components
{
InternalChildren = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 1,
Children = new[]
{
minBackground = new Box
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(0.5f),
},
maxBackground = new Box
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(0.5f),
},
}
},
new FillFlowContainer
new CircularContainer
{
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
Masking = true,
// Stops artifacting from boxes drawn behind wrong colour boxes (and edge pixels adding up to higher opacity).
Padding = new MarginPadding(-0.1f),
Child = bufferedContent = new BufferedContainer(pixelSnapping: true, cachedFrameBuffer: true)
{
minDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Range),
maxDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Range)
AutoSizeAxes = Axes.Both,
Children = new[]
{
minBackground = new Box
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(1, 0.5f),
},
maxBackground = new Box
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(1, 0.5f),
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
minDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Range),
maxDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Range)
}
}
}
}
}
},
};
}
@ -121,6 +127,8 @@ namespace osu.Game.Screens.OnlinePlay.Components
minBackground.Colour = colours.ForStarDifficulty(minDifficulty.Stars);
maxBackground.Colour = colours.ForStarDifficulty(maxDifficulty.Stars);
bufferedContent.ForceRedraw();
}
protected override void Dispose(bool isDisposing)

View File

@ -568,6 +568,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
[Description("Off")]
Off = 0,
[Description("10 seconds")]
Seconds10 = 10,
[Description("30 seconds")]
Seconds30 = 30,

View File

@ -29,7 +29,6 @@ using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Overlays.Volume;
using osu.Game.Performance;
using osu.Game.Scoring;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Play.PlayerSettings;
using osu.Game.Skinning;
@ -80,8 +79,6 @@ namespace osu.Game.Screens.Play
private FillFlowContainer disclaimers = null!;
private OsuScrollContainer settingsScroll = null!;
private Bindable<ScoreInfo?> lastScore = null!;
private Bindable<bool> showStoryboards = null!;
private bool backgroundBrightnessReduction;
@ -183,8 +180,6 @@ namespace osu.Game.Screens.Play
{
muteWarningShownOnce = sessionStatics.GetBindable<bool>(Static.MutedAudioNotificationShownOnce);
batteryWarningShownOnce = sessionStatics.GetBindable<bool>(Static.LowBatteryNotificationShownOnce);
lastScore = sessionStatics.GetBindable<ScoreInfo?>(Static.LastLocalUserScore);
showStoryboards = config.GetBindable<bool>(OsuSetting.ShowStoryboard);
const float padding = 25;
@ -354,8 +349,6 @@ namespace osu.Game.Screens.Play
highPerformanceSession?.Dispose();
highPerformanceSession = null;
lastScore.Value = null;
return base.OnExiting(e);
}

View File

@ -15,6 +15,7 @@ using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
@ -36,6 +37,8 @@ namespace osu.Game.Screens.Play.PlayerSettings
{
public Bindable<ScoreInfo?> ReferenceScore { get; } = new Bindable<ScoreInfo?>();
private Bindable<ScoreInfo?> lastAppliedScore { get; } = new Bindable<ScoreInfo?>();
public BindableDouble Current { get; } = new BindableDouble
{
MinValue = -50,
@ -100,6 +103,12 @@ namespace osu.Game.Screens.Play.PlayerSettings
};
}
[BackgroundDependencyLoader]
private void load(SessionStatics statics)
{
statics.BindWith(Static.LastAppliedOffsetScore, lastAppliedScore);
}
protected override void LoadComplete()
{
base.LoadComplete();
@ -176,6 +185,9 @@ namespace osu.Game.Screens.Play.PlayerSettings
if (score.NewValue == null)
return;
if (score.NewValue.Equals(lastAppliedScore.Value))
return;
if (!score.NewValue.BeatmapInfo.AsNonNull().Equals(beatmap.Value.BeatmapInfo))
return;
@ -230,7 +242,11 @@ namespace osu.Game.Screens.Play.PlayerSettings
useAverageButton = new SettingsButton
{
Text = BeatmapOffsetControlStrings.CalibrateUsingLastPlay,
Action = () => Current.Value = lastPlayBeatmapOffset - lastPlayAverage,
Action = () =>
{
Current.Value = lastPlayBeatmapOffset - lastPlayAverage;
lastAppliedScore.Value = ReferenceScore.Value;
},
Enabled = { Value = !Precision.AlmostEquals(lastPlayAverage, 0, Current.Precision / 2) }
},
globalOffsetText = new LinkFlowContainer

View File

@ -54,6 +54,10 @@ namespace osu.Game.Users
}
[MessagePackObject]
[Union(12, typeof(InSoloGame))]
[Union(23, typeof(InMultiplayerGame))]
[Union(24, typeof(SpectatingMultiplayerGame))]
[Union(31, typeof(InPlaylistGame))]
public abstract class InGame : UserActivity
{
[Key(0)]

View File

@ -20,24 +20,24 @@
<ItemGroup Label="Package References">
<PackageReference Include="AutoMapper" Version="13.0.1" />
<PackageReference Include="DiffPlex" Version="1.7.2" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.70" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.72" />
<PackageReference Include="Humanizer" Version="2.14.1" />
<PackageReference Include="MessagePack" Version="2.5.187" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.10" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="8.0.10" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="8.0.10" />
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="8.0.10" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageReference Include="MessagePack" Version="3.1.1" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="9.0.0" />
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Toolkit.HighPerformance" Version="7.1.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="ppy.LocalisationAnalyser" Version="2024.802.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Realm" Version="11.5.0" />
<PackageReference Include="Realm" Version="20.1.0" />
<PackageReference Include="ppy.osu.Framework" Version="2024.1224.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2024.1224.0" />
<PackageReference Include="Sentry" Version="4.13.0" />
<PackageReference Include="Sentry" Version="5.0.0" />
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->
<PackageReference Include="SharpCompress" Version="0.38.0" />
<PackageReference Include="NUnit" Version="3.14.0" />

View File

@ -157,5 +157,9 @@
<string>public.app-category.music-games</string>
<key>LSSupportsOpeningDocumentsInPlace</key>
<false/>
<!-- Game mode is supposed to be automatically enabled by the app category specification above,
but for some reason it needs to be explicitly enabled with this key. -->
<key>GCSupportsGameMode</key>
<true/>
</dict>
</plist>