1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-18 11:52:54 +08:00

Merge branch 'master' into show-in-windows-association-menus and adapt ApplicationCapability

This commit is contained in:
Susko3 2025-01-08 16:15:22 +00:00
commit dc23534a57
58 changed files with 766 additions and 340 deletions

View File

@ -51,8 +51,11 @@ dotnet_diagnostic.IDE1006.severity = warning
# Too many noisy warnings for parsing/formatting numbers # Too many noisy warnings for parsing/formatting numbers
dotnet_diagnostic.CA1305.severity = none 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 # CA1507: Use nameof to express symbol names
# Flaggs serialization name attributes # Flags serialization name attributes
dotnet_diagnostic.CA1507.severity = suggestion dotnet_diagnostic.CA1507.severity = suggestion
# CA1806: Do not ignore method results # CA1806: Do not ignore method results

View File

@ -9,9 +9,9 @@
<GenerateProgramFile>false</GenerateProgramFile> <GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup Label="Package References"> <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="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\osu.Game.Rulesets.EmptyFreeform\osu.Game.Rulesets.EmptyFreeform.csproj" /> <ProjectReference Include="..\osu.Game.Rulesets.EmptyFreeform\osu.Game.Rulesets.EmptyFreeform.csproj" />

View File

@ -9,9 +9,9 @@
<GenerateProgramFile>false</GenerateProgramFile> <GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup Label="Package References"> <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="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" /> <ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />

View File

@ -9,9 +9,9 @@
<GenerateProgramFile>false</GenerateProgramFile> <GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup Label="Package References"> <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="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\osu.Game.Rulesets.EmptyScrolling\osu.Game.Rulesets.EmptyScrolling.csproj" /> <ProjectReference Include="..\osu.Game.Rulesets.EmptyScrolling\osu.Game.Rulesets.EmptyScrolling.csproj" />

View File

@ -9,9 +9,9 @@
<GenerateProgramFile>false</GenerateProgramFile> <GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup Label="Package References"> <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="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" /> <ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />

View File

@ -61,14 +61,13 @@ namespace osu.Desktop.Windows
/// Installs file and URI associations. /// Installs file and URI associations.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Call <see cref="UpdateDescriptions"/> in a timely fashion to keep descriptions up-to-date and localised. /// Call <see cref="LocaliseDescriptions"/> in a timely fashion to keep descriptions up-to-date and localised.
/// </remarks> /// </remarks>
public static void InstallAssociations() public static void InstallAssociations()
{ {
try try
{ {
updateAssociations(); updateAssociations();
updateDescriptions(null); // write default descriptions in case `UpdateDescriptions()` is not called.
NotifyShellUpdate(); NotifyShellUpdate();
} }
catch (Exception e) catch (Exception e)
@ -81,17 +80,13 @@ namespace osu.Desktop.Windows
/// Updates associations with latest definitions. /// Updates associations with latest definitions.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Call <see cref="UpdateDescriptions"/> in a timely fashion to keep descriptions up-to-date and localised. /// Call <see cref="LocaliseDescriptions"/> in a timely fashion to keep descriptions up-to-date and localised.
/// </remarks> /// </remarks>
public static void UpdateAssociations() public static void UpdateAssociations()
{ {
try try
{ {
updateAssociations(); updateAssociations();
// TODO: Remove once UpdateDescriptions() is called as specified in the xmldoc.
updateDescriptions(null); // always write default descriptions, in case of updating from an older version in which file associations were not implemented/installed
NotifyShellUpdate(); NotifyShellUpdate();
} }
catch (Exception e) catch (Exception e)
@ -100,11 +95,19 @@ namespace osu.Desktop.Windows
} }
} }
public static void UpdateDescriptions(LocalisationManager localisationManager) // TODO: call this sometime.
public static void LocaliseDescriptions(LocalisationManager localisationManager)
{ {
try try
{ {
updateDescriptions(localisationManager); application_capability.LocaliseDescription(localisationManager);
foreach (var association in file_associations)
association.LocaliseDescription(localisationManager);
foreach (var association in uri_associations)
association.LocaliseDescription(localisationManager);
NotifyShellUpdate(); NotifyShellUpdate();
} }
catch (Exception e) catch (Exception e)
@ -152,19 +155,6 @@ namespace osu.Desktop.Windows
application_capability.RegisterUriAssociations(uri_associations); application_capability.RegisterUriAssociations(uri_associations);
} }
private static void updateDescriptions(LocalisationManager? localisation)
{
application_capability.UpdateDescription(getLocalisedString(application_capability.Description));
foreach (var association in file_associations)
association.UpdateDescription(getLocalisedString(association.Description));
foreach (var association in uri_associations)
association.UpdateDescription(getLocalisedString(association.Description));
string getLocalisedString(LocalisableString s) => localisation?.GetLocalisedString(s) ?? s.ToString();
}
#region Native interop #region Native interop
[DllImport("Shell32.dll")] [DllImport("Shell32.dll")]
@ -188,26 +178,37 @@ namespace osu.Desktop.Windows
#endregion #endregion
private record ApplicationCapability(string UniqueName, string CapabilityPath, LocalisableString Description) private class ApplicationCapability
{ {
private string uniqueName { get; }
private string capabilityPath { get; }
private LocalisableString description { get; }
public ApplicationCapability(string uniqueName, string capabilityPath, LocalisableString description)
{
this.uniqueName = uniqueName;
this.capabilityPath = capabilityPath;
this.description = description;
}
/// <summary> /// <summary>
/// Registers an application capability according to <see href="https://learn.microsoft.com/en-us/windows/win32/shell/default-programs#registering-an-application-for-use-with-default-programs"> /// Registers an application capability according to <see href="https://learn.microsoft.com/en-us/windows/win32/shell/default-programs#registering-an-application-for-use-with-default-programs">
/// Registering an Application for Use with Default Programs</see>. /// Registering an Application for Use with Default Programs</see>.
/// </summary> /// </summary>
public void Install() public void Install()
{ {
using (Registry.CurrentUser.CreateSubKey(CapabilityPath)) using (var capability = Registry.CurrentUser.CreateSubKey(capabilityPath))
{ {
// create an empty "capability" key, other methods will fill it with information capability.SetValue(@"ApplicationDescription", description.ToString());
} }
using (var registeredApplications = Registry.CurrentUser.OpenSubKey(software_registered_applications, true)) using (var registeredApplications = Registry.CurrentUser.OpenSubKey(software_registered_applications, true))
registeredApplications?.SetValue(UniqueName, CapabilityPath); registeredApplications?.SetValue(uniqueName, capabilityPath);
} }
public void RegisterFileAssociations(FileAssociation[] associations) public void RegisterFileAssociations(FileAssociation[] associations)
{ {
using var capability = Registry.CurrentUser.OpenSubKey(CapabilityPath, true); using var capability = Registry.CurrentUser.OpenSubKey(capabilityPath, true);
if (capability == null) return; if (capability == null) return;
using var fileAssociations = capability.CreateSubKey(@"FileAssociations"); using var fileAssociations = capability.CreateSubKey(@"FileAssociations");
@ -218,7 +219,7 @@ namespace osu.Desktop.Windows
public void RegisterUriAssociations(UriAssociation[] associations) public void RegisterUriAssociations(UriAssociation[] associations)
{ {
using var capability = Registry.CurrentUser.OpenSubKey(CapabilityPath, true); using var capability = Registry.CurrentUser.OpenSubKey(capabilityPath, true);
if (capability == null) return; if (capability == null) return;
using var urlAssociations = capability.CreateSubKey(@"UrlAssociations"); using var urlAssociations = capability.CreateSubKey(@"UrlAssociations");
@ -227,27 +228,38 @@ namespace osu.Desktop.Windows
urlAssociations.SetValue(association.Protocol, association.ProgramId); urlAssociations.SetValue(association.Protocol, association.ProgramId);
} }
public void UpdateDescription(string description) public void LocaliseDescription(LocalisationManager localisationManager)
{ {
using (var capability = Registry.CurrentUser.OpenSubKey(CapabilityPath, true)) using (var capability = Registry.CurrentUser.OpenSubKey(capabilityPath, true))
{ {
capability?.SetValue(@"ApplicationDescription", description); capability?.SetValue(@"ApplicationDescription", localisationManager.GetLocalisedString(description));
} }
} }
public void Uninstall() public void Uninstall()
{ {
using (var registeredApplications = Registry.CurrentUser.OpenSubKey(software_registered_applications, true)) using (var registeredApplications = Registry.CurrentUser.OpenSubKey(software_registered_applications, true))
registeredApplications?.DeleteValue(UniqueName, throwOnMissingValue: false); registeredApplications?.DeleteValue(uniqueName, throwOnMissingValue: false);
Registry.CurrentUser.DeleteSubKeyTree(CapabilityPath, throwOnMissingSubKey: false); Registry.CurrentUser.DeleteSubKeyTree(capabilityPath, throwOnMissingSubKey: false);
} }
} }
private record FileAssociation(string Extension, LocalisableString Description, string IconPath) private class FileAssociation
{ {
public string ProgramId => $@"{program_id_file_prefix}{Extension}"; public string ProgramId => $@"{program_id_file_prefix}{Extension}";
public string Extension { get; }
private LocalisableString description { get; }
private string iconPath { get; }
public FileAssociation(string extension, LocalisableString description, string iconPath)
{
Extension = extension;
this.description = description;
this.iconPath = iconPath;
}
/// <summary> /// <summary>
/// Installs a file extension association in accordance with https://learn.microsoft.com/en-us/windows/win32/com/-progid--key /// Installs a file extension association in accordance with https://learn.microsoft.com/en-us/windows/win32/com/-progid--key
/// </summary> /// </summary>
@ -259,8 +271,10 @@ namespace osu.Desktop.Windows
// register a program id for the given extension // register a program id for the given extension
using (var programKey = classes.CreateSubKey(ProgramId)) using (var programKey = classes.CreateSubKey(ProgramId))
{ {
programKey.SetValue(null, description.ToString());
using (var defaultIconKey = programKey.CreateSubKey(default_icon)) using (var defaultIconKey = programKey.CreateSubKey(default_icon))
defaultIconKey.SetValue(null, IconPath); defaultIconKey.SetValue(null, iconPath);
using (var openCommandKey = programKey.CreateSubKey(SHELL_OPEN_COMMAND)) using (var openCommandKey = programKey.CreateSubKey(SHELL_OPEN_COMMAND))
openCommandKey.SetValue(null, $@"""{exe_path}"" ""%1"""); openCommandKey.SetValue(null, $@"""{exe_path}"" ""%1""");
@ -280,13 +294,13 @@ namespace osu.Desktop.Windows
} }
} }
public void UpdateDescription(string description) public void LocaliseDescription(LocalisationManager localisationManager)
{ {
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true); using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
if (classes == null) return; if (classes == null) return;
using (var programKey = classes.OpenSubKey(ProgramId, true)) using (var programKey = classes.OpenSubKey(ProgramId, true))
programKey?.SetValue(null, description); programKey?.SetValue(null, localisationManager.GetLocalisedString(description));
} }
/// <summary> /// <summary>
@ -307,13 +321,24 @@ namespace osu.Desktop.Windows
} }
} }
private record UriAssociation(string Protocol, LocalisableString Description, string IconPath) private class UriAssociation
{ {
/// <summary> /// <summary>
/// "The <c>URL Protocol</c> string value indicates that this key declares a custom pluggable protocol handler." /// "The <c>URL Protocol</c> string value indicates that this key declares a custom pluggable protocol handler."
/// See https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85). /// See https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85).
/// </summary> /// </summary>
public const string URL_PROTOCOL = @"URL Protocol"; private const string url_protocol = @"URL Protocol";
public string Protocol { get; }
private LocalisableString description { get; }
private string iconPath { get; }
public UriAssociation(string protocol, LocalisableString description, string iconPath)
{
Protocol = protocol;
this.description = description;
this.iconPath = iconPath;
}
public string ProgramId => $@"{program_id_protocol_prefix}.{Protocol}"; public string ProgramId => $@"{program_id_protocol_prefix}.{Protocol}";
@ -327,7 +352,8 @@ namespace osu.Desktop.Windows
using (var protocolKey = classes.CreateSubKey(Protocol)) using (var protocolKey = classes.CreateSubKey(Protocol))
{ {
protocolKey.SetValue(URL_PROTOCOL, string.Empty); protocolKey.SetValue(null, $@"URL:{description}");
protocolKey.SetValue(url_protocol, string.Empty);
// clear out old data // clear out old data
protocolKey.DeleteSubKeyTree(default_icon, throwOnMissingSubKey: false); protocolKey.DeleteSubKeyTree(default_icon, throwOnMissingSubKey: false);
@ -338,20 +364,20 @@ namespace osu.Desktop.Windows
using (var programKey = classes.CreateSubKey(ProgramId)) using (var programKey = classes.CreateSubKey(ProgramId))
{ {
using (var defaultIconKey = programKey.CreateSubKey(default_icon)) using (var defaultIconKey = programKey.CreateSubKey(default_icon))
defaultIconKey.SetValue(null, IconPath); defaultIconKey.SetValue(null, iconPath);
using (var openCommandKey = programKey.CreateSubKey(SHELL_OPEN_COMMAND)) using (var openCommandKey = programKey.CreateSubKey(SHELL_OPEN_COMMAND))
openCommandKey.SetValue(null, $@"""{exe_path}"" ""%1"""); openCommandKey.SetValue(null, $@"""{exe_path}"" ""%1""");
} }
} }
public void UpdateDescription(string description) public void LocaliseDescription(LocalisationManager localisationManager)
{ {
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true); using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
if (classes == null) return; if (classes == null) return;
using (var protocolKey = classes.OpenSubKey(Protocol, true)) using (var protocolKey = classes.OpenSubKey(Protocol, true))
protocolKey?.SetValue(null, $@"URL:{description}"); protocolKey?.SetValue(null, $@"URL:{localisationManager.GetLocalisedString(description)}");
} }
public void Uninstall() public void Uninstall()

View File

@ -24,9 +24,9 @@
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" /> <ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <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="DiscordRichPresence" Version="1.2.1.24" />
<PackageReference Include="Velopack" Version="0.0.915" /> <PackageReference Include="Velopack" Version="0.0.1053" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Resources"> <ItemGroup Label="Resources">
<EmbeddedResource Include="lazer.ico" /> <EmbeddedResource Include="lazer.ico" />

View File

@ -9,7 +9,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" /> <PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
<PackageReference Include="nunit" Version="3.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>
<ItemGroup> <ItemGroup>

View File

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

View File

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

View File

@ -1,10 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <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="Moq" Version="4.18.4" />
<PackageReference Include="NUnit" Version="3.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>
<PropertyGroup Label="Project"> <PropertyGroup Label="Project">
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>

View File

@ -626,7 +626,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
{ {
if (BodyPiece.ReceivePositionalInputAt(screenSpacePos)) if (BodyPiece.ReceivePositionalInputAt(screenSpacePos) && DrawableObject.Body.Alpha > 0)
return true; return true;
if (ControlPointVisualiser == null) 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, // If samples aren't available at the exact start time of the object,
// use samples (without additions) in the closest original hit object instead // 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

@ -382,6 +382,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
repeat.SuppressHitAnimations(); repeat.SuppressHitAnimations();
TailCircle.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() internal void RestoreHitAnimations()

View File

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

View File

@ -3,10 +3,10 @@
using System; using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Utils;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;
@ -40,37 +40,31 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
private void load(DrawableHitObject drawableObject) private void load(DrawableHitObject drawableObject)
{ {
drawableRepeat = (DrawableSliderRepeat)drawableObject; drawableRepeat = (DrawableSliderRepeat)drawableObject;
drawableRepeat.ApplyCustomUpdateState += updateStateTransforms;
} }
private void updateStateTransforms(DrawableHitObject hitObject, ArmedState state) protected override void Update()
{ {
const double move_out_duration = 35; base.Update();
const double move_in_duration = 250;
const double total = 300;
switch (state) if (Time.Current >= drawableRepeat.HitStateUpdateTime && drawableRepeat.State.Value == ArmedState.Hit)
{ {
case ArmedState.Idle: double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration);
InternalChild.ScaleTo(1.3f, move_out_duration, Easing.Out) Scale = new Vector2(Interpolation.ValueAt(Time.Current, 1, 1.5f, drawableRepeat.HitStateUpdateTime, drawableRepeat.HitStateUpdateTime + animDuration, 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;
} }
} else
{
const float scale_amount = 1.3f;
protected override void Dispose(bool isDisposing) const double move_out_duration = 35;
{ const double move_in_duration = 250;
base.Dispose(isDisposing); const double total = 300;
if (drawableRepeat.IsNotNull()) double loopCurrentTime = (Time.Current - drawableRepeat.AnimationStartTime.Value) % total;
drawableRepeat.ApplyCustomUpdateState -= updateStateTransforms; 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;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Utils;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Legacy 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; textureIsDefaultSkin = skin is ISkinTransformer transformer && transformer.Skin is DefaultLegacySkin;
drawableObject.ApplyCustomUpdateState += updateStateTransforms;
shouldRotate = skinSource.GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version)?.Value <= 1; 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 = drawableRepeat.AccentColour.GetBoundCopy();
accentColour.BindValueChanged(c => 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); }, true);
} }
@ -80,36 +80,32 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
drawableRepeat.DrawableSlider.OverlayElementContainer.Add(proxy); drawableRepeat.DrawableSlider.OverlayElementContainer.Add(proxy);
} }
private void updateStateTransforms(DrawableHitObject hitObject, ArmedState state) protected override void Update()
{ {
const double duration = 300; base.Update();
const float rotation = 5.625f;
switch (state) if (Time.Current >= drawableRepeat.HitStateUpdateTime && drawableRepeat.State.Value == ArmedState.Hit)
{ {
case ArmedState.Idle: double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration);
if (shouldRotate) arrow.Scale = new Vector2(Interpolation.ValueAt(Time.Current, 1, 1.4f, drawableRepeat.HitStateUpdateTime, drawableRepeat.HitStateUpdateTime + animDuration, Easing.Out));
{ }
InternalChild.ScaleTo(1.3f) else
.RotateTo(rotation) {
.Then() const double duration = 300;
.ScaleTo(1f, duration) const float rotation = 5.625f;
.RotateTo(-rotation, duration)
.Loop();
}
else
{
InternalChild.ScaleTo(1.3f).Then()
.ScaleTo(1f, duration, Easing.Out)
.Loop();
}
break; double loopCurrentTime = (Time.Current - drawableRepeat.AnimationStartTime.Value) % duration;
case ArmedState.Hit: // Reference: https://github.com/peppy/osu-stable-reference/blob/2280c4c436f80d04f9c79d3c905db00ac2902273/osu!/GameplayElements/HitObjects/Osu/HitCircleSliderEnd.cs#L79-L96
double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration); if (shouldRotate)
InternalChild.ScaleTo(1.4f, animDuration, Easing.Out); {
break; 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()) if (drawableRepeat.IsNotNull())
{ {
drawableRepeat.HitObjectApplied -= onHitObjectApplied; drawableRepeat.HitObjectApplied -= onHitObjectApplied;
drawableRepeat.ApplyCustomUpdateState -= updateStateTransforms;
} }
} }
} }

View File

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

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

@ -539,5 +539,85 @@ namespace osu.Game.Tests.Editing
Assert.That(beatmap.Breaks[0].EndTime, Is.EqualTo(5000 - OsuHitObject.PREEMPT_MAX)); Assert.That(beatmap.Breaks[0].EndTime, Is.EqualTo(5000 - OsuHitObject.PREEMPT_MAX));
}); });
} }
[Test]
public void TestPuttingObjectBetweenBreakEndAndAnotherObjectForcesNewCombo()
{
var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new EditorBeatmap(new Beatmap
{
ControlPointInfo = controlPoints,
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
Difficulty =
{
ApproachRate = 10,
},
HitObjects =
{
new HitCircle { StartTime = 1000, NewCombo = true },
new HitCircle { StartTime = 4500 },
new HitCircle { StartTime = 5000, NewCombo = true },
},
Breaks =
{
new BreakPeriod(2000, 4000),
}
});
foreach (var ho in beatmap.HitObjects)
ho.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
beatmapProcessor.PreProcess();
beatmapProcessor.PostProcess();
Assert.Multiple(() =>
{
Assert.That(((HitCircle)beatmap.HitObjects[1]).NewCombo, Is.True);
Assert.That(((HitCircle)beatmap.HitObjects[2]).NewCombo, Is.True);
Assert.That(((HitCircle)beatmap.HitObjects[0]).ComboIndex, Is.EqualTo(1));
Assert.That(((HitCircle)beatmap.HitObjects[1]).ComboIndex, Is.EqualTo(2));
Assert.That(((HitCircle)beatmap.HitObjects[2]).ComboIndex, Is.EqualTo(3));
});
}
[Test]
public void TestAutomaticallyInsertedBreakForcesNewCombo()
{
var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new EditorBeatmap(new Beatmap
{
ControlPointInfo = controlPoints,
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
Difficulty =
{
ApproachRate = 10,
},
HitObjects =
{
new HitCircle { StartTime = 1000, NewCombo = true },
new HitCircle { StartTime = 5000 },
},
});
foreach (var ho in beatmap.HitObjects)
ho.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
beatmapProcessor.PreProcess();
beatmapProcessor.PostProcess();
Assert.Multiple(() =>
{
Assert.That(beatmap.Breaks, Has.Count.EqualTo(1));
Assert.That(((HitCircle)beatmap.HitObjects[1]).NewCombo, Is.True);
Assert.That(((HitCircle)beatmap.HitObjects[0]).ComboIndex, Is.EqualTo(1));
Assert.That(((HitCircle)beatmap.HitObjects[1]).ComboIndex, Is.EqualTo(2));
});
}
} }
} }

View File

@ -19,6 +19,7 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Backgrounds;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components;
using osu.Game.Screens.Edit.Components.Timelines.Summary; using osu.Game.Screens.Edit.Components.Timelines.Summary;
using osu.Game.Screens.Edit.GameplayTest; using osu.Game.Screens.Edit.GameplayTest;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
@ -127,6 +128,35 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("sample playback re-enabled", () => !Editor.SamplePlaybackDisabled.Value); AddAssert("sample playback re-enabled", () => !Editor.SamplePlaybackDisabled.Value);
} }
[Test]
public void TestGameplayTestResetsPlaybackSpeedAdjustment()
{
AddStep("start track", () => EditorClock.Start());
AddStep("change playback speed", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<PlaybackControl.PlaybackTabControl.PlaybackTabItem>().First());
InputManager.Click(MouseButton.Left);
});
AddAssert("track playback rate is 0.25x", () => Beatmap.Value.Track.AggregateTempo.Value, () => Is.EqualTo(0.25));
AddStep("click test gameplay button", () =>
{
var button = Editor.ChildrenOfType<TestGameplayButton>().Single();
InputManager.MoveMouseTo(button);
InputManager.Click(MouseButton.Left);
});
EditorPlayer editorPlayer = null;
AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null);
AddAssert("editor track stopped", () => !EditorClock.IsRunning);
AddAssert("track playback rate is 1x", () => Beatmap.Value.Track.AggregateTempo.Value, () => Is.EqualTo(1));
AddStep("exit player", () => editorPlayer.Exit());
AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
AddAssert("track playback rate is 0.25x", () => Beatmap.Value.Track.AggregateTempo.Value, () => Is.EqualTo(0.25));
}
[TestCase(2000)] // chosen to be after last object in the map [TestCase(2000)] // chosen to be after last object in the map
[TestCase(22000)] // chosen to be in the middle of the last spinner [TestCase(22000)] // chosen to be in the middle of the last spinner
public void TestGameplayTestAtEndOfBeatmap(int offsetFromEnd) public void TestGameplayTestAtEndOfBeatmap(int offsetFromEnd)

View File

@ -26,7 +26,7 @@ namespace osu.Game.Tests.Visual.Menus
protected OsuScreenStack IntroStack; protected OsuScreenStack IntroStack;
private IntroScreen intro; protected IntroScreen Intro { get; private set; }
[Cached(typeof(INotificationOverlay))] [Cached(typeof(INotificationOverlay))]
private NotificationOverlay notifications; private NotificationOverlay notifications;
@ -62,22 +62,9 @@ namespace osu.Game.Tests.Visual.Menus
[Test] [Test]
public virtual void TestPlayIntro() public virtual void TestPlayIntro()
{ {
AddStep("restart sequence", () => RestartIntro();
{
logo.FinishTransforms();
logo.IsTracking = false;
IntroStack?.Expire(); WaitForMenu();
Add(IntroStack = new OsuScreenStack
{
RelativeSizeAxes = Axes.Both,
});
IntroStack.Push(intro = CreateScreen());
});
AddUntilStep("wait for menu", () => intro.DidLoadMenu);
} }
[Test] [Test]
@ -103,18 +90,18 @@ namespace osu.Game.Tests.Visual.Menus
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
}); });
IntroStack.Push(intro = CreateScreen()); IntroStack.Push(Intro = CreateScreen());
}); });
AddStep("trigger failure", () => AddStep("trigger failure", () =>
{ {
trackResetDelegate = Scheduler.AddDelayed(() => trackResetDelegate = Scheduler.AddDelayed(() =>
{ {
intro.Beatmap.Value.Track.Seek(0); Intro.Beatmap.Value.Track.Seek(0);
}, 0, true); }, 0, true);
}); });
AddUntilStep("wait for menu", () => intro.DidLoadMenu); WaitForMenu();
if (IntroReliesOnTrack) if (IntroReliesOnTrack)
AddUntilStep("wait for notification", () => notifications.UnreadCount.Value == 1); AddUntilStep("wait for notification", () => notifications.UnreadCount.Value == 1);
@ -122,6 +109,29 @@ namespace osu.Game.Tests.Visual.Menus
AddStep("uninstall delegate", () => trackResetDelegate?.Cancel()); 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(); 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 NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Components; 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 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(); RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Child = new StarRatingRangeDisplay(SelectedRoom.Value) Spacing = new Vector2(10),
Children = new Drawable[]
{ {
Anchor = Anchor.Centre, new StarRatingRangeDisplay(room)
Origin = Anchor.Centre {
}; 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] [Test]
@ -33,10 +75,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
AddStep("set playlist", () => AddStep("set playlist", () =>
{ {
SelectedRoom.Value!.Playlist = room.Playlist =
[ [
new PlaylistItem(new BeatmapInfo { StarRating = min }), new PlaylistItem(new BeatmapInfo { StarRating = min }) { ID = TestResources.GetNextTestID() },
new PlaylistItem(new BeatmapInfo { StarRating = max }), new PlaylistItem(new BeatmapInfo { StarRating = max }) { ID = TestResources.GetNextTestID() },
]; ];
}); });
} }

View File

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

View File

@ -1,11 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <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="DeepEqual" Version="4.2.1" />
<PackageReference Include="Nito.AsyncEx" Version="5.1.2" /> <PackageReference Include="Nito.AsyncEx" Version="5.1.2" />
<PackageReference Include="NUnit" Version="3.14.0" /> <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" /> <PackageReference Include="Moq" Version="4.18.4" />
</ItemGroup> </ItemGroup>
<PropertyGroup Label="Project"> <PropertyGroup Label="Project">

View File

@ -4,9 +4,9 @@
<StartupObject>osu.Game.Tournament.Tests.TournamentTestRunner</StartupObject> <StartupObject>osu.Game.Tournament.Tests.TournamentTestRunner</StartupObject>
</PropertyGroup> </PropertyGroup>
<ItemGroup Label="Package References"> <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="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
</ItemGroup> </ItemGroup>
<PropertyGroup Label="Project"> <PropertyGroup Label="Project">
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>

View File

@ -33,12 +33,12 @@ namespace osu.Game.Audio
/// <summary> /// <summary>
/// All valid sample addition constants. /// All valid sample addition constants.
/// </summary> /// </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> /// <summary>
/// All valid bank constants. /// All valid bank constants.
/// </summary> /// </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> /// <summary>
/// The name of the sample to load. /// The name of the sample to load.

View File

@ -266,7 +266,7 @@ namespace osu.Game.Database
/// <para> /// <para>
/// If a write transaction did not modify any objects in this <see cref="IRealmCollection{T}" />, the callback is not invoked at all. /// 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>. /// 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>
/// <para> /// <para>
/// At the time when the block is called, the <see cref="IRealmCollection{T}" /> object will be fully evaluated /// 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. /// 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" />. /// To stop receiving notifications, call <see cref="IDisposable.Dispose" />.
/// </returns> /// </returns>
/// <seealso cref="Realms.CollectionExtensions.SubscribeForNotifications{T}(IList{T}, NotificationCallbackDelegate{T})" /> /// <seealso cref="Realms.CollectionExtensions.SubscribeForNotifications{T}(IList{T}, NotificationCallbackDelegate{T},KeyPathsCollection?)" />
/// <seealso cref="Realms.CollectionExtensions.SubscribeForNotifications{T}(IQueryable{T}, NotificationCallbackDelegate{T})" /> /// <seealso cref="Realms.CollectionExtensions.SubscribeForNotifications{T}(IQueryable{T}, NotificationCallbackDelegate{T},KeyPathsCollection?)" />
#pragma warning restore RS0030 #pragma warning restore RS0030
public static IDisposable QueryAsyncWithNotifications<T>(this IRealmCollection<T> collection, NotificationCallbackDelegate<T> callback) public static IDisposable QueryAsyncWithNotifications<T>(this IRealmCollection<T> collection, NotificationCallbackDelegate<T> callback)
where T : RealmObjectBase where T : RealmObjectBase

View File

@ -46,7 +46,8 @@ namespace osu.Game.Database
} }
public IRealmCollection<T> Freeze() => throw new NotImplementedException(); 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 bool IsValid => throw new NotImplementedException();
public Realm Realm => throw new NotImplementedException(); public Realm Realm => throw new NotImplementedException();
public ObjectSchema ObjectSchema => throw new NotImplementedException(); public ObjectSchema ObjectSchema => throw new NotImplementedException();

View File

@ -15,10 +15,9 @@ namespace osu.Game.Localisation
public static LocalisableString Header => new TranslatableString(getKey(@"header"), @"Import"); public static LocalisableString Header => new TranslatableString(getKey(@"header"), @"Import");
/// <summary> /// <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> /// </summary>
public static LocalisableString Description => new TranslatableString(getKey(@"description"), 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.");
@"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> /// <summary>
/// "previous osu! install" /// "previous osu! install"
@ -38,8 +37,7 @@ namespace osu.Game.Localisation
/// <summary> /// <summary>
/// "Your import will continue in the background. Check on its progress in the notifications sidebar!" /// "Your import will continue in the background. Check on its progress in the notifications sidebar!"
/// </summary> /// </summary>
public static LocalisableString ImportInProgress => 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!");
new TranslatableString(getKey(@"import_in_progress"), @"Your import will continue in the background. Check on its progress in the notifications sidebar!");
/// <summary> /// <summary>
/// "calculating..." /// "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!"); public static LocalisableString LinkTypeNotSupported => new TranslatableString(getKey(@"unsupported_link_type"), @"This link type is not yet supported!");
/// <summary> /// <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> /// </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); 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> /// <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> /// </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); 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> /// <summary>
/// "You are now running osu! {0}. /// "You are now running osu! {0}.
/// Click to see what's new!" /// Click to see what&#39;s new!"
/// </summary> /// </summary>
public static LocalisableString GameVersionAfterUpdate(string version) => new TranslatableString(getKey(@"game_version_after_update"), @"You are now running osu! {0}. 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); Click to see what's new!", version);

View File

@ -10,10 +10,12 @@ using osu.Game.Configuration;
namespace osu.Game.Online.API 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; var primitiveFormatter = PrimitiveObjectFormatter.Instance;
writer.WriteArrayHeader(value.Count); writer.WriteArrayHeader(value.Count);

View File

@ -4,13 +4,16 @@
using System; using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Localisation; using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Dialog; using osu.Game.Overlays.Dialog;
using osu.Game.Overlays.Notifications;
using WebCommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings; using WebCommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings;
namespace osu.Game.Online.Chat namespace osu.Game.Online.Chat
@ -23,9 +26,15 @@ namespace osu.Game.Online.Chat
[Resolved] [Resolved]
private Clipboard clipboard { get; set; } = null!; private Clipboard clipboard { get; set; } = null!;
[Resolved(CanBeNull = true)] [Resolved]
private IDialogOverlay? dialogOverlay { get; set; } private IDialogOverlay? dialogOverlay { get; set; }
[Resolved]
private INotificationOverlay? notificationOverlay { get; set; }
[Resolved]
private IAPIProvider api { get; set; } = null!;
private Bindable<bool> externalLinkWarning = null!; private Bindable<bool> externalLinkWarning = null!;
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
@ -34,9 +43,51 @@ namespace osu.Game.Online.Chat
externalLinkWarning = config.GetBindable<bool>(OsuSetting.ExternalLinkWarning); 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))); dialogOverlay.Push(new ExternalLinkDialog(url, () => host.OpenUrlExternally(url), () => clipboard.SetText(url)));
else else
host.OpenUrlExternally(url); 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 namespace osu.Game.Online.Multiplayer.MatchTypes.TeamVersus
{ {
[MessagePackObject]
public class TeamVersusUserState : MatchUserState public class TeamVersusUserState : MatchUserState
{ {
[Key(0)] [Key(0)]

View File

@ -18,7 +18,6 @@ using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Configuration; using osu.Framework.Configuration;
using osu.Framework.Extensions;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -516,32 +515,7 @@ namespace osu.Game
onScreenDisplay.Display(new CopyUrlToast()); onScreenDisplay.Display(new CopyUrlToast());
}); });
public void OpenUrlExternally(string url, bool forceBypassExternalUrlWarning = false) => waitForReady(() => externalLinkOpener, _ => public void OpenUrlExternally(string url, LinkWarnMode warnMode = LinkWarnMode.Default) => waitForReady(() => externalLinkOpener, _ => externalLinkOpener.OpenUrlExternally(url, warnMode));
{
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);
});
/// <summary> /// <summary>
/// Open a specific channel in chat. /// Open a specific channel in chat.
@ -1340,7 +1314,7 @@ namespace osu.Game
IconColour = Colours.YellowDark, IconColour = Colours.YellowDark,
Activated = () => Activated = () =>
{ {
OpenUrlExternally("https://opentabletdriver.net/Tablets", true); OpenUrlExternally("https://opentabletdriver.net/Tablets", LinkWarnMode.NeverWarn);
return true; return true;
} }
})); }));

View File

@ -315,6 +315,7 @@ namespace osu.Game
dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, realm, API, LocalConfig)); 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.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(BeatmapDownloader = new BeatmapModelDownloader(BeatmapManager, API));
dependencies.Cache(ScoreDownloader = new ScoreModelDownloader(ScoreManager, 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.Graphics.UserInterface;
using osu.Game.Localisation; using osu.Game.Localisation;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.Chat;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
using osu.Game.Resources.Localisation.Web; using osu.Game.Resources.Localisation.Web;
using osuTK; using osuTK;
@ -213,7 +214,7 @@ namespace osu.Game.Overlays.AccountCreation
if (!string.IsNullOrEmpty(errors.Message)) if (!string.IsNullOrEmpty(errors.Message))
passwordDescription.AddErrors(new[] { 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 else

View File

@ -11,6 +11,7 @@ using osu.Framework.Input.Events;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Online.Chat;
using osu.Game.Resources.Localisation.Web; using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.Profile.Header.Components namespace osu.Game.Overlays.Profile.Header.Components
@ -87,7 +88,8 @@ namespace osu.Game.Overlays.Profile.Header.Components
{ {
background.Colour = colours.Pink; 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) protected override bool OnHover(HoverEvent e)

View File

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

View File

@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Edit.Checks
++objectsWithoutHitsounds; ++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); private bool isHitnormal(HitSampleInfo sample) => sample.Name.Contains(HitSampleInfo.HIT_NORMAL);
public abstract class IssueTemplateLongPeriod : IssueTemplate public abstract class IssueTemplateLongPeriod : IssueTemplate

View File

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

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -18,8 +17,6 @@ namespace osu.Game.Screens.Edit.Components
protected readonly IBindable<WorkingBeatmap> Beatmap = new Bindable<WorkingBeatmap>(); protected readonly IBindable<WorkingBeatmap> Beatmap = new Bindable<WorkingBeatmap>();
protected readonly IBindable<Track> Track = new Bindable<Track>();
public readonly Drawable Background; public readonly Drawable Background;
private readonly Container content; private readonly Container content;
@ -45,10 +42,9 @@ namespace osu.Game.Screens.Edit.Components
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(IBindable<WorkingBeatmap> beatmap, EditorClock clock) private void load(IBindable<WorkingBeatmap> beatmap)
{ {
Beatmap.BindTo(beatmap); Beatmap.BindTo(beatmap);
Track.BindTo(clock.Track);
} }
} }
} }

View File

@ -8,6 +8,7 @@ using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
@ -75,7 +76,7 @@ namespace osu.Game.Screens.Edit.Components
} }
}; };
Track.BindValueChanged(tr => tr.NewValue?.AddAdjustment(AdjustableProperty.Tempo, tempoAdjustment), true); editorClock.AudioAdjustments.AddAdjustment(AdjustableProperty.Tempo, tempoAdjustment);
if (editor != null) if (editor != null)
currentScreenMode.BindTo(editor.Mode); currentScreenMode.BindTo(editor.Mode);
@ -105,7 +106,8 @@ namespace osu.Game.Screens.Edit.Components
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
Track.Value?.RemoveAdjustment(AdjustableProperty.Tempo, tempoAdjustment); if (editorClock.IsNotNull())
editorClock.AudioAdjustments.RemoveAdjustment(AdjustableProperty.Tempo, tempoAdjustment);
base.Dispose(isDisposing); base.Dispose(isDisposing);
} }
@ -148,7 +150,7 @@ namespace osu.Game.Screens.Edit.Components
public LocalisableString TooltipText { get; set; } public LocalisableString TooltipText { get; set; }
} }
private partial class PlaybackTabControl : OsuTabControl<double> public partial class PlaybackTabControl : OsuTabControl<double>
{ {
private static readonly double[] tempo_values = { 0.25, 0.5, 0.75, 1 }; private static readonly double[] tempo_values = { 0.25, 0.5, 0.75, 1 };

View File

@ -3,8 +3,8 @@
using System; using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osuTK; using osuTK;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -26,7 +26,8 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
[Resolved] [Resolved]
protected EditorBeatmap EditorBeatmap { get; private set; } = null!; protected EditorBeatmap EditorBeatmap { get; private set; } = null!;
protected readonly IBindable<Track> Track = new Bindable<Track>(); [Resolved]
private EditorClock editorClock { get; set; } = null!;
private readonly Container<T> content; private readonly Container<T> content;
@ -35,22 +36,17 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
public TimelinePart(Container<T>? content = null) public TimelinePart(Container<T>? content = null)
{ {
AddInternal(this.content = content ?? new Container<T> { RelativeSizeAxes = Axes.Both }); AddInternal(this.content = content ?? new Container<T> { RelativeSizeAxes = Axes.Both });
beatmap.ValueChanged += _ =>
{
updateRelativeChildSize();
};
Track.ValueChanged += _ => updateRelativeChildSize();
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(IBindable<WorkingBeatmap> beatmap, EditorClock clock) private void load(IBindable<WorkingBeatmap> beatmap)
{ {
this.beatmap.BindTo(beatmap); this.beatmap.BindTo(beatmap);
LoadBeatmap(EditorBeatmap); LoadBeatmap(EditorBeatmap);
Track.BindTo(clock.Track); this.beatmap.ValueChanged += _ => updateRelativeChildSize();
editorClock.TrackChanged += updateRelativeChildSize;
updateRelativeChildSize();
} }
private void updateRelativeChildSize() private void updateRelativeChildSize()
@ -68,5 +64,13 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
{ {
content.Clear(); content.Clear();
} }
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (editorClock.IsNotNull())
editorClock.TrackChanged -= updateRelativeChildSize;
}
} }
} }

View File

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

View File

@ -409,7 +409,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private void createStateBindables() private void createStateBindables()
{ {
foreach (string sampleName in HitSampleInfo.AllAdditions) foreach (string sampleName in HitSampleInfo.ALL_ADDITIONS)
{ {
var bindable = new Bindable<TernaryState> var bindable = new Bindable<TernaryState>
{ {
@ -433,7 +433,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
selectionSampleStates[sampleName] = bindable; 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() private void updateTernaryStates()

View File

@ -3,9 +3,9 @@
using System; using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Audio; using osu.Framework.Graphics.Audio;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -49,6 +49,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[Resolved] [Resolved]
private EditorBeatmap editorBeatmap { get; set; } = null!; private EditorBeatmap editorBeatmap { get; set; } = null!;
[Resolved]
private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;
/// <summary> /// <summary>
/// The timeline's scroll position in the last frame. /// The timeline's scroll position in the last frame.
/// </summary> /// </summary>
@ -86,8 +89,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private double trackLengthForZoom; private double trackLengthForZoom;
private readonly IBindable<Track> track = new Bindable<Track>();
public Timeline(Drawable userContent) public Timeline(Drawable userContent)
{ {
this.userContent = userContent; this.userContent = userContent;
@ -101,7 +102,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(IBindable<WorkingBeatmap> beatmap, OsuColour colours, OverlayColourProvider colourProvider, OsuConfigManager config) private void load(OsuColour colours, OverlayColourProvider colourProvider, OsuConfigManager config)
{ {
CentreMarker centreMarker; CentreMarker centreMarker;
@ -150,16 +151,18 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
controlPointsVisible = config.GetBindable<bool>(OsuSetting.EditorTimelineShowTimingChanges); controlPointsVisible = config.GetBindable<bool>(OsuSetting.EditorTimelineShowTimingChanges);
ticksVisible = config.GetBindable<bool>(OsuSetting.EditorTimelineShowTicks); ticksVisible = config.GetBindable<bool>(OsuSetting.EditorTimelineShowTicks);
track.BindTo(editorClock.Track); editorClock.TrackChanged += updateWaveform;
track.BindValueChanged(_ => updateWaveform();
{
waveform.Waveform = beatmap.Value.Waveform;
Scheduler.AddOnce(applyVisualOffset, beatmap);
}, true);
Zoom = (float)(defaultTimelineZoom * editorBeatmap.TimelineZoom); Zoom = (float)(defaultTimelineZoom * editorBeatmap.TimelineZoom);
} }
private void updateWaveform()
{
waveform.Waveform = beatmap.Value.Waveform;
Scheduler.AddOnce(applyVisualOffset, beatmap);
}
private void applyVisualOffset(IBindable<WorkingBeatmap> beatmap) private void applyVisualOffset(IBindable<WorkingBeatmap> beatmap)
{ {
waveform.RelativePositionAxes = Axes.X; waveform.RelativePositionAxes = Axes.X;
@ -334,5 +337,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
double time = TimeAtPosition(Content.ToLocalSpace(screenSpacePosition).X); double time = TimeAtPosition(Content.ToLocalSpace(screenSpacePosition).X);
return new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(time)); return new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(time));
} }
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (editorClock.IsNotNull())
editorClock.TrackChanged -= updateWaveform;
}
} }
} }

View File

@ -155,9 +155,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
if (hitObject.GetEndTime() < editorClock.CurrentTime - timeline.VisibleRange / 2) if (hitObject.GetEndTime() < editorClock.CurrentTime - timeline.VisibleRange / 2)
break; 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); 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); 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)) var node = hasRepeats.NodeSamples[i];
minimumGap = Math.Max(minimumGap, absolute_minimum_gap + sample.Bank.Length * 3);
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

@ -861,6 +861,7 @@ namespace osu.Game.Screens.Edit
{ {
base.OnResuming(e); base.OnResuming(e);
dimBackground(); dimBackground();
clock.BindAdjustments();
} }
private void dimBackground() private void dimBackground()
@ -925,6 +926,10 @@ namespace osu.Game.Screens.Edit
base.OnSuspending(e); base.OnSuspending(e);
clock.Stop(); clock.Stop();
refetchBeatmap(); refetchBeatmap();
// unfortunately ordering matters here.
// this unbind MUST happen after `refetchBeatmap()`, because along other things, `refetchBeatmap()` causes a global working beatmap change,
// which causes `EditorClock` to reload the track and automatically reapply adjustments to it.
clock.UnbindAdjustments();
} }
private void refetchBeatmap() private void refetchBeatmap()

View File

@ -41,6 +41,7 @@ namespace osu.Game.Screens.Edit
rulesetBeatmapProcessor?.PostProcess(); rulesetBeatmapProcessor?.PostProcess();
autoGenerateBreaks(); autoGenerateBreaks();
ensureNewComboAfterBreaks();
} }
private void autoGenerateBreaks() private void autoGenerateBreaks()
@ -100,5 +101,40 @@ namespace osu.Game.Screens.Edit
Beatmap.Breaks.Add(breakPeriod); Beatmap.Breaks.Add(breakPeriod);
} }
} }
private void ensureNewComboAfterBreaks()
{
var breakEnds = Beatmap.Breaks.Select(b => b.EndTime).OrderBy(t => t).ToList();
if (breakEnds.Count == 0)
return;
int currentBreak = 0;
IHasComboInformation? lastObj = null;
bool comboInformationUpdateRequired = false;
foreach (var hitObject in Beatmap.HitObjects)
{
if (hitObject is not IHasComboInformation hasCombo)
continue;
if (currentBreak < breakEnds.Count && hitObject.StartTime >= breakEnds[currentBreak])
{
if (!hasCombo.NewCombo)
{
hasCombo.NewCombo = true;
comboInformationUpdateRequired = true;
}
currentBreak += 1;
}
if (comboInformationUpdateRequired)
hasCombo.UpdateComboInformation(lastObj);
lastObj = hasCombo;
}
}
} }
} }

View File

@ -6,6 +6,8 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Audio;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -23,12 +25,15 @@ namespace osu.Game.Screens.Edit
/// </summary> /// </summary>
public partial class EditorClock : CompositeComponent, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock public partial class EditorClock : CompositeComponent, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock
{ {
public IBindable<Track> Track => track; [CanBeNull]
public event Action TrackChanged;
private readonly Bindable<Track> track = new Bindable<Track>(); private readonly Bindable<Track> track = new Bindable<Track>();
public double TrackLength => track.Value?.IsLoaded == true ? track.Value.Length : 60000; public double TrackLength => track.Value?.IsLoaded == true ? track.Value.Length : 60000;
public AudioAdjustments AudioAdjustments { get; } = new AudioAdjustments();
public ControlPointInfo ControlPointInfo => Beatmap.ControlPointInfo; public ControlPointInfo ControlPointInfo => Beatmap.ControlPointInfo;
public IBeatmap Beatmap { get; set; } public IBeatmap Beatmap { get; set; }
@ -56,6 +61,8 @@ namespace osu.Game.Screens.Edit
underlyingClock = new FramedBeatmapClock(applyOffsets: true, requireDecoupling: true); underlyingClock = new FramedBeatmapClock(applyOffsets: true, requireDecoupling: true);
AddInternal(underlyingClock); AddInternal(underlyingClock);
track.BindValueChanged(_ => TrackChanged?.Invoke());
} }
/// <summary> /// <summary>
@ -208,7 +215,16 @@ namespace osu.Game.Screens.Edit
} }
} }
public void ResetSpeedAdjustments() => underlyingClock.ResetSpeedAdjustments(); public void BindAdjustments() => track.Value?.BindAdjustments(AudioAdjustments);
public void UnbindAdjustments() => track.Value?.UnbindAdjustments(AudioAdjustments);
public void ResetSpeedAdjustments()
{
AudioAdjustments.RemoveAllAdjustments(AdjustableProperty.Frequency);
AudioAdjustments.RemoveAllAdjustments(AdjustableProperty.Tempo);
underlyingClock.ResetSpeedAdjustments();
}
double IAdjustableClock.Rate double IAdjustableClock.Rate
{ {
@ -231,8 +247,12 @@ namespace osu.Game.Screens.Edit
public void ChangeSource(IClock source) public void ChangeSource(IClock source)
{ {
UnbindAdjustments();
track.Value = source as Track; track.Value = source as Track;
underlyingClock.ChangeSource(source); underlyingClock.ChangeSource(source);
BindAdjustments();
} }
public IClock Source => underlyingClock.Source; public IClock Source => underlyingClock.Source;

View File

@ -4,8 +4,8 @@
using System; using System;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Audio; using osu.Framework.Graphics.Audio;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -305,7 +305,8 @@ namespace osu.Game.Screens.Edit.Timing
[Resolved] [Resolved]
private IBindable<WorkingBeatmap> beatmap { get; set; } = null!; private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;
private readonly IBindable<Track> track = new Bindable<Track>(); [Resolved]
private EditorClock editorClock { get; set; } = null!;
public WaveformRow(bool isMainRow) public WaveformRow(bool isMainRow)
{ {
@ -313,7 +314,7 @@ namespace osu.Game.Screens.Edit.Timing
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(EditorClock clock) private void load()
{ {
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
@ -343,13 +344,16 @@ namespace osu.Game.Screens.Edit.Timing
Colour = colourProvider.Content2 Colour = colourProvider.Content2
} }
}; };
track.BindTo(clock.Track);
} }
protected override void LoadComplete() protected override void LoadComplete()
{ {
track.ValueChanged += _ => waveformGraph.Waveform = beatmap.Value.Waveform; editorClock.TrackChanged += updateWaveform;
}
private void updateWaveform()
{
waveformGraph.Waveform = beatmap.Value.Waveform;
} }
public int BeatIndex { set => beatIndexText.Text = value.ToString(); } public int BeatIndex { set => beatIndexText.Text = value.ToString(); }
@ -363,6 +367,14 @@ namespace osu.Game.Screens.Edit.Timing
get => waveformGraph.X; get => waveformGraph.X;
set => waveformGraph.X = value; set => waveformGraph.X = value;
} }
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (editorClock.IsNotNull())
editorClock.TrackChanged -= updateWaveform;
}
} }
} }
} }

View File

@ -20,6 +20,7 @@ using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.Localisation; using osu.Game.Localisation;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Overlays; using osu.Game.Overlays;
@ -170,7 +171,14 @@ namespace osu.Game.Screens.Menu
if (s.Beatmaps.Count == 0) if (s.Beatmaps.Count == 0)
return; 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; return UsingThemedIntro = initialBeatmap != null;
@ -188,6 +196,20 @@ namespace osu.Game.Screens.Menu
[Resolved] [Resolved]
private INotificationOverlay notifications { get; set; } 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() private void ensureEventuallyArrivingAtMenu()
{ {
// This intends to handle the case where an intro may get stuck. // This intends to handle the case where an intro may get stuck.

View File

@ -14,7 +14,6 @@ using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osuTK; using osuTK;
using Container = osu.Framework.Graphics.Containers.Container;
namespace osu.Game.Screens.OnlinePlay.Components namespace osu.Game.Screens.OnlinePlay.Components
{ {
@ -30,6 +29,8 @@ namespace osu.Game.Screens.OnlinePlay.Components
private StarRatingDisplay maxDisplay = null!; private StarRatingDisplay maxDisplay = null!;
private Drawable maxBackground = null!; private Drawable maxBackground = null!;
private BufferedContainer bufferedContent = null!;
public StarRatingRangeDisplay(Room room) public StarRatingRangeDisplay(Room room)
{ {
this.room = room; this.room = room;
@ -41,38 +42,43 @@ namespace osu.Game.Screens.OnlinePlay.Components
{ {
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
new Container new CircularContainer
{
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
{ {
AutoSizeAxes = Axes.Both, 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), AutoSizeAxes = Axes.Both,
maxDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Range) 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); minBackground.Colour = colours.ForStarDifficulty(minDifficulty.Stars);
maxBackground.Colour = colours.ForStarDifficulty(maxDifficulty.Stars); maxBackground.Colour = colours.ForStarDifficulty(maxDifficulty.Stars);
bufferedContent.ForceRedraw();
} }
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)

View File

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

View File

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