1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-20 20:33:21 +08:00

Merge branch 'master' into replay-analysis-settings

This commit is contained in:
Dean Herbert 2024-09-04 17:52:19 +09:00
commit 0e16508fd6
No known key found for this signature in database
25 changed files with 364 additions and 378 deletions

View File

@ -80,7 +80,7 @@ namespace osu.Android
host.Window.CursorState |= CursorState.Hidden;
}
protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager();
protected override UpdateManager CreateUpdateManager() => new MobileUpdateNotifier();
protected override BatteryInfo CreateBatteryInfo() => new AndroidBatteryInfo();

View File

@ -2,10 +2,10 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.Versioning;
using System.Threading.Tasks;
using Microsoft.Win32;
using osu.Desktop.Performance;
using osu.Desktop.Security;
@ -102,35 +102,13 @@ namespace osu.Desktop
if (!string.IsNullOrEmpty(packageManaged))
return new NoActionUpdateManager();
switch (RuntimeInfo.OS)
{
case RuntimeInfo.Platform.Windows:
Debug.Assert(OperatingSystem.IsWindows());
return new SquirrelUpdateManager();
default:
return new SimpleUpdateManager();
}
return new VelopackUpdateManager();
}
public override bool RestartAppWhenExited()
{
switch (RuntimeInfo.OS)
{
case RuntimeInfo.Platform.Windows:
Debug.Assert(OperatingSystem.IsWindows());
// Of note, this is an async method in squirrel that adds an arbitrary delay before returning
// likely to ensure the external process is in a good state.
//
// We're not waiting on that here, but the outro playing before the actual exit should be enough
// to cover this.
Squirrel.UpdateManager.RestartAppWhenExited().FireAndForget();
return true;
}
return base.RestartAppWhenExited();
Task.Run(() => Velopack.UpdateExe.Start()).FireAndForget();
return true;
}
protected override void LoadComplete()

View File

@ -3,7 +3,6 @@
using System;
using System.IO;
using System.Runtime.Versioning;
using osu.Desktop.LegacyIpc;
using osu.Desktop.Windows;
using osu.Framework;
@ -14,7 +13,7 @@ using osu.Game;
using osu.Game.IPC;
using osu.Game.Tournament;
using SDL;
using Squirrel;
using Velopack;
namespace osu.Desktop
{
@ -31,19 +30,11 @@ namespace osu.Desktop
[STAThread]
public static void Main(string[] args)
{
/*
* WARNING: DO NOT PLACE **ANY** CODE ABOVE THE FOLLOWING BLOCK!
*
* Logic handling Squirrel MUST run before EVERYTHING if you do not want to break it.
* To be more precise: Squirrel is internally using a rather... crude method to determine whether it is running under NUnit,
* namely by checking loaded assemblies:
* https://github.com/clowd/Clowd.Squirrel/blob/24427217482deeeb9f2cacac555525edfc7bd9ac/src/Squirrel/SimpleSplat/PlatformModeDetector.cs#L17-L32
*
* If it finds ANY assembly from the ones listed above - REGARDLESS of the reason why it is loaded -
* the app will then do completely broken things like:
* - not creating system shortcuts (as the logic is if'd out if "running tests")
* - not exiting after the install / first-update / uninstall hooks are ran (as the `Environment.Exit()` calls are if'd out if "running tests")
*/
// IMPORTANT DON'T IGNORE: For general sanity, velopack's setup needs to run before anything else.
// This has bitten us in the rear before (bricked updater), and although the underlying issue from
// last time has been fixed, let's not tempt fate.
setupVelopack();
if (OperatingSystem.IsWindows())
{
var windowsVersion = Environment.OSVersion.Version;
@ -66,8 +57,6 @@ namespace osu.Desktop
return;
}
}
setupSquirrel();
}
// NVIDIA profiles are based on the executable name of a process.
@ -177,32 +166,14 @@ namespace osu.Desktop
return false;
}
[SupportedOSPlatform("windows")]
private static void setupSquirrel()
private static void setupVelopack()
{
SquirrelAwareApp.HandleEvents(onInitialInstall: (_, tools) =>
{
tools.CreateShortcutForThisExe();
tools.CreateUninstallerRegistryEntry();
WindowsAssociationManager.InstallAssociations();
}, onAppUpdate: (_, tools) =>
{
tools.CreateUninstallerRegistryEntry();
WindowsAssociationManager.UpdateAssociations();
}, onAppUninstall: (_, tools) =>
{
tools.RemoveShortcutForThisExe();
tools.RemoveUninstallerRegistryEntry();
WindowsAssociationManager.UninstallAssociations();
}, onEveryRun: (_, _, _) =>
{
// While setting the `ProcessAppUserModelId` fixes duplicate icons/shortcuts on the taskbar, it currently
// causes the right-click context menu to function incorrectly.
//
// This may turn out to be non-required after an alternative solution is implemented.
// see https://github.com/clowd/Clowd.Squirrel/issues/24
// tools.SetProcessAppUserModelId();
});
VelopackApp
.Build()
.WithFirstRun(v =>
{
if (OperatingSystem.IsWindows()) WindowsAssociationManager.InstallAssociations();
}).Run();
}
}
}

View File

@ -1,180 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Runtime.Versioning;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Logging;
using osu.Game;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Screens.Play;
using Squirrel.SimpleSplat;
using Squirrel.Sources;
using LogLevel = Squirrel.SimpleSplat.LogLevel;
using UpdateManager = osu.Game.Updater.UpdateManager;
namespace osu.Desktop.Updater
{
[SupportedOSPlatform("windows")]
public partial class SquirrelUpdateManager : UpdateManager
{
private Squirrel.UpdateManager? updateManager;
private INotificationOverlay notificationOverlay = null!;
public Task PrepareUpdateAsync() => Squirrel.UpdateManager.RestartAppWhenExited();
private static readonly Logger logger = Logger.GetLogger("updater");
/// <summary>
/// Whether an update has been downloaded but not yet applied.
/// </summary>
private bool updatePending;
private readonly SquirrelLogger squirrelLogger = new SquirrelLogger();
[Resolved]
private OsuGameBase game { get; set; } = null!;
[Resolved]
private ILocalUserPlayInfo? localUserInfo { get; set; }
[BackgroundDependencyLoader]
private void load(INotificationOverlay notifications)
{
notificationOverlay = notifications;
SquirrelLocator.CurrentMutable.Register(() => squirrelLogger, typeof(ILogger));
}
protected override async Task<bool> PerformUpdateCheck() => await checkForUpdateAsync().ConfigureAwait(false);
private async Task<bool> checkForUpdateAsync(bool useDeltaPatching = true, UpdateProgressNotification? notification = null)
{
// should we schedule a retry on completion of this check?
bool scheduleRecheck = true;
const string? github_token = null; // TODO: populate.
try
{
// Avoid any kind of update checking while gameplay is running.
if (localUserInfo?.IsPlaying.Value == true)
return false;
updateManager ??= new Squirrel.UpdateManager(new GithubSource(@"https://github.com/ppy/osu", github_token, false), @"osulazer");
var info = await updateManager.CheckForUpdate(!useDeltaPatching).ConfigureAwait(false);
if (info.ReleasesToApply.Count == 0)
{
if (updatePending)
{
// the user may have dismissed the completion notice, so show it again.
notificationOverlay.Post(new UpdateApplicationCompleteNotification
{
Activated = () =>
{
restartToApplyUpdate();
return true;
},
});
return true;
}
// no updates available. bail and retry later.
return false;
}
scheduleRecheck = false;
if (notification == null)
{
notification = new UpdateProgressNotification
{
CompletionClickAction = restartToApplyUpdate,
};
Schedule(() => notificationOverlay.Post(notification));
}
notification.StartDownload();
try
{
await updateManager.DownloadReleases(info.ReleasesToApply, p => notification.Progress = p / 100f).ConfigureAwait(false);
notification.StartInstall();
await updateManager.ApplyReleases(info, p => notification.Progress = p / 100f).ConfigureAwait(false);
notification.State = ProgressNotificationState.Completed;
updatePending = true;
}
catch (Exception e)
{
if (useDeltaPatching)
{
logger.Add(@"delta patching failed; will attempt full download!");
// could fail if deltas are unavailable for full update path (https://github.com/Squirrel/Squirrel.Windows/issues/959)
// try again without deltas.
await checkForUpdateAsync(false, notification).ConfigureAwait(false);
}
else
{
// In the case of an error, a separate notification will be displayed.
notification.FailDownload();
Logger.Error(e, @"update failed!");
}
}
}
catch (Exception)
{
// we'll ignore this and retry later. can be triggered by no internet connection or thread abortion.
scheduleRecheck = true;
}
finally
{
if (scheduleRecheck)
{
// check again in 30 minutes.
Scheduler.AddDelayed(() => Task.Run(async () => await checkForUpdateAsync().ConfigureAwait(false)), 60000 * 30);
}
}
return true;
}
private bool restartToApplyUpdate()
{
PrepareUpdateAsync()
.ContinueWith(_ => Schedule(() => game.AttemptExit()));
return true;
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
updateManager?.Dispose();
}
private class SquirrelLogger : ILogger, IDisposable
{
public LogLevel Level { get; set; } = LogLevel.Info;
public void Write(string message, LogLevel logLevel)
{
if (logLevel < Level)
return;
logger.Add(message);
}
public void Dispose()
{
}
}
}
}

View File

@ -0,0 +1,131 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Logging;
using osu.Game;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Screens.Play;
using Velopack;
using Velopack.Sources;
namespace osu.Desktop.Updater
{
public partial class VelopackUpdateManager : Game.Updater.UpdateManager
{
private readonly UpdateManager updateManager;
private INotificationOverlay notificationOverlay = null!;
[Resolved]
private OsuGameBase game { get; set; } = null!;
[Resolved]
private ILocalUserPlayInfo? localUserInfo { get; set; }
private UpdateInfo? pendingUpdate;
public VelopackUpdateManager()
{
updateManager = new UpdateManager(new GithubSource(@"https://github.com/ppy/osu", null, false), new UpdateOptions
{
AllowVersionDowngrade = true,
});
}
[BackgroundDependencyLoader]
private void load(INotificationOverlay notifications)
{
notificationOverlay = notifications;
}
protected override async Task<bool> PerformUpdateCheck() => await checkForUpdateAsync().ConfigureAwait(false);
private async Task<bool> checkForUpdateAsync(UpdateProgressNotification? notification = null)
{
// should we schedule a retry on completion of this check?
bool scheduleRecheck = true;
try
{
// Avoid any kind of update checking while gameplay is running.
if (localUserInfo?.IsPlaying.Value == true)
return false;
if (pendingUpdate != null)
{
// If there is an update pending restart, show the notification to restart again.
notificationOverlay.Post(new UpdateApplicationCompleteNotification
{
Activated = () =>
{
restartToApplyUpdate();
return true;
}
});
return true;
}
pendingUpdate = await updateManager.CheckForUpdatesAsync().ConfigureAwait(false);
// Handle no updates available.
if (pendingUpdate == null)
return false;
scheduleRecheck = false;
if (notification == null)
{
notification = new UpdateProgressNotification
{
CompletionClickAction = restartToApplyUpdate,
};
Schedule(() => notificationOverlay.Post(notification));
}
notification.StartDownload();
try
{
await updateManager.DownloadUpdatesAsync(pendingUpdate, p => notification.Progress = p / 100f).ConfigureAwait(false);
notification.State = ProgressNotificationState.Completed;
}
catch (Exception e)
{
// In the case of an error, a separate notification will be displayed.
notification.FailDownload();
Logger.Error(e, @"update failed!");
}
}
catch (Exception e)
{
// we'll ignore this and retry later. can be triggered by no internet connection or thread abortion.
scheduleRecheck = true;
Logger.Log($@"update check failed ({e.Message})");
}
finally
{
if (scheduleRecheck)
{
// check again in 30 minutes.
Scheduler.AddDelayed(() => Task.Run(async () => await checkForUpdateAsync().ConfigureAwait(false)), 60000 * 30);
}
}
return true;
}
private bool restartToApplyUpdate()
{
// TODO: Migrate this to async flow whenever available (see https://github.com/ppy/osu/pull/28743#discussion_r1740505665).
// Currently there's an internal Thread.Sleep(300) which will cause a stutter when the user clicks to restart.
updateManager.WaitExitThenApplyUpdates(pendingUpdate?.TargetFullRelease);
Schedule(() => game.AttemptExit());
return true;
}
}
}

View File

@ -23,9 +23,9 @@
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Clowd.Squirrel" Version="2.11.1" />
<PackageReference Include="System.IO.Packaging" Version="8.0.0" />
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
<PackageReference Include="Velopack" Version="0.0.598-g933b2ab" />
</ItemGroup>
<ItemGroup Label="Resources">
<EmbeddedResource Include="lazer.ico" />

View File

@ -18,7 +18,9 @@ namespace osu.Game.Rulesets.Catch.Edit
// The implementation below is probably correct but should be checked if/when exposed via controls.
float expectedDistance = DurationToDistance(before, after.StartTime - before.GetEndTime());
float actualDistance = Math.Abs(((CatchHitObject)before).EffectiveX - ((CatchHitObject)after).EffectiveX);
float previousEndX = (before as JuiceStream)?.EndX ?? ((CatchHitObject)before).EffectiveX;
float actualDistance = Math.Abs(previousEndX - ((CatchHitObject)after).EffectiveX);
return actualDistance / expectedDistance;
}

View File

@ -85,9 +85,25 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
protected void SetTexture(Texture? texture, Texture? overlayTexture)
{
colouredSprite.Texture = texture;
overlaySprite.Texture = overlayTexture;
hyperSprite.Texture = texture;
// Sizes are reset due to an arguable osu!framework bug where Sprite retains the size of the first set texture.
if (colouredSprite.Texture != texture)
{
colouredSprite.Size = Vector2.Zero;
colouredSprite.Texture = texture;
}
if (overlaySprite.Texture != overlayTexture)
{
overlaySprite.Size = Vector2.Zero;
overlaySprite.Texture = overlayTexture;
}
if (hyperSprite.Texture != texture)
{
hyperSprite.Size = Vector2.Zero;
hyperSprite.Texture = texture;
}
}
}
}

View File

@ -0,0 +1,47 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Beatmaps;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Editor
{
public partial class TestSceneSliderChangeStates : TestSceneOsuEditor
{
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
[TestCase(SplineType.Catmull)]
[TestCase(SplineType.BSpline)]
[TestCase(SplineType.Linear)]
[TestCase(SplineType.PerfectCurve)]
public void TestSliderRetainsCurveTypes(SplineType splineType)
{
Slider? slider = null;
PathType pathType = new PathType(splineType);
AddStep("add slider", () => EditorBeatmap.Add(slider = new Slider
{
StartTime = 500,
Path = new SliderPath(new[]
{
new PathControlPoint(Vector2.Zero, pathType),
new PathControlPoint(new Vector2(200, 0), pathType),
})
}));
AddAssert("slider has correct spline type", () => ((Slider)EditorBeatmap.HitObjects[0]).Path.ControlPoints.All(p => p.Type == pathType));
AddStep("remove object", () => EditorBeatmap.Remove(slider));
AddAssert("slider removed", () => EditorBeatmap.HitObjects.Count == 0);
addUndoSteps();
AddAssert("slider not removed", () => EditorBeatmap.HitObjects.Count == 1);
AddAssert("slider has correct spline type", () => ((Slider)EditorBeatmap.HitObjects[0]).Path.ControlPoints.All(p => p.Type == pathType));
}
private void addUndoSteps() => AddStep("undo", () => Editor.Undo());
}
}

View File

@ -7,6 +7,7 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
@ -58,19 +59,19 @@ namespace osu.Game.Tests.Visual.UserInterface
{
SelectedMods.Value = new[] { new OsuModDoubleTime() };
panel.Enabled.Value = true;
panel.ExpandedState.Value = ModCustomisationPanel.ModCustomisationPanelState.Expanded;
panel.ExpandedState.Value = ModCustomisationPanel.ModCustomisationPanelState.ExpandedByMod;
});
AddStep("set DA", () =>
{
SelectedMods.Value = new Mod[] { new OsuModDifficultyAdjust() };
panel.Enabled.Value = true;
panel.ExpandedState.Value = ModCustomisationPanel.ModCustomisationPanelState.Expanded;
panel.ExpandedState.Value = ModCustomisationPanel.ModCustomisationPanelState.ExpandedByMod;
});
AddStep("set FL+WU+DA+AD", () =>
{
SelectedMods.Value = new Mod[] { new OsuModFlashlight(), new ModWindUp(), new OsuModDifficultyAdjust(), new OsuModApproachDifferent() };
panel.Enabled.Value = true;
panel.ExpandedState.Value = ModCustomisationPanel.ModCustomisationPanelState.Expanded;
panel.ExpandedState.Value = ModCustomisationPanel.ModCustomisationPanelState.ExpandedByMod;
});
AddStep("set empty", () =>
{
@ -120,7 +121,7 @@ namespace osu.Game.Tests.Visual.UserInterface
}
[Test]
public void TestExpandedStatePersistsWhenClicked()
public void TestHoverExpandsAndCollapsesWhenHeaderTouched()
{
AddStep("add customisable mod", () =>
{
@ -128,34 +129,20 @@ namespace osu.Game.Tests.Visual.UserInterface
panel.Enabled.Value = true;
});
AddStep("hover header", () => InputManager.MoveMouseTo(header));
checkExpanded(true);
AddStep("click", () => InputManager.Click(MouseButton.Left));
checkExpanded(false);
AddStep("click", () => InputManager.Click(MouseButton.Left));
checkExpanded(true);
AddStep("move away", () => InputManager.MoveMouseTo(Vector2.One));
checkExpanded(true);
AddStep("click", () => InputManager.Click(MouseButton.Left));
checkExpanded(false);
}
[Test]
public void TestHoverExpandsAndCollapsesWhenHeaderClicked()
{
AddStep("add customisable mod", () =>
AddStep("touch header", () =>
{
SelectedMods.Value = new[] { new OsuModDoubleTime() };
panel.Enabled.Value = true;
var touch = new Touch(TouchSource.Touch1, header.ScreenSpaceDrawQuad.Centre);
InputManager.BeginTouch(touch);
Schedule(() => InputManager.EndTouch(touch));
});
AddStep("hover header", () => InputManager.MoveMouseTo(header));
checkExpanded(true);
AddStep("click", () => InputManager.Click(MouseButton.Left));
AddStep("touch away from header", () =>
{
var touch = new Touch(TouchSource.Touch1, header.ScreenSpaceDrawQuad.TopLeft - new Vector2(10));
InputManager.BeginTouch(touch);
Schedule(() => InputManager.EndTouch(touch));
});
checkExpanded(false);
}

View File

@ -241,12 +241,8 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("select difficulty adjust via panel", () => getPanelForMod(typeof(OsuModDifficultyAdjust)).TriggerClick());
assertCustomisationToggleState(disabled: false, active: true);
AddStep("dismiss mod customisation via toggle", () =>
{
InputManager.MoveMouseTo(modSelectOverlay.ChildrenOfType<ModCustomisationHeader>().Single());
InputManager.Click(MouseButton.Left);
});
assertCustomisationToggleState(disabled: false, active: false);
AddStep("move mouse away", () => InputManager.MoveMouseTo(Vector2.Zero));
assertCustomisationToggleState(disabled: false, active: true);
AddStep("reset mods", () => SelectedMods.SetDefault());
AddStep("select difficulty adjust via panel", () => getPanelForMod(typeof(OsuModDifficultyAdjust)).TriggerClick());
@ -664,7 +660,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("select DT", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime() });
AddAssert("DT selected", () => modSelectOverlay.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value), () => Is.EqualTo(1));
AddStep("open customisation area", () => modSelectOverlay.ChildrenOfType<ModCustomisationHeader>().Single().TriggerClick());
AddStep("open customisation area", () => InputManager.MoveMouseTo(modSelectOverlay.ChildrenOfType<ModCustomisationHeader>().Single()));
assertCustomisationToggleState(disabled: false, active: true);
AddStep("hover over mod settings slider", () =>
@ -976,7 +972,7 @@ namespace osu.Game.Tests.Visual.UserInterface
createScreen();
AddStep("select DT", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime() });
AddStep("open customisation panel", () => this.ChildrenOfType<ModCustomisationHeader>().Single().TriggerClick());
AddStep("open customisation panel", () => InputManager.MoveMouseTo(this.ChildrenOfType<ModCustomisationHeader>().Single()));
AddAssert("search lost focus", () => !this.ChildrenOfType<ShearedSearchTextBox>().Single().HasFocus);
AddStep("press tab", () => InputManager.Key(Key.Tab));
@ -991,15 +987,13 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("press backspace", () => InputManager.Key(Key.BackSpace));
AddAssert("mods not deselected", () => SelectedMods.Value.Single() is OsuModDoubleTime);
AddStep("move mouse to scroll bar", () => InputManager.MoveMouseTo(modSelectOverlay.ChildrenOfType<ModSelectOverlay.ColumnScrollContainer>().Single().ScreenSpaceDrawQuad.BottomLeft + new Vector2(10f, -5f)));
AddStep("move mouse to customisation panel", () => InputManager.MoveMouseTo(modSelectOverlay.ChildrenOfType<ModCustomisationSection>().First()));
AddStep("scroll down", () => InputManager.ScrollVerticalBy(-10f));
AddAssert("column not scrolled", () => modSelectOverlay.ChildrenOfType<ModSelectOverlay.ColumnScrollContainer>().Single().IsScrolledToStart());
AddStep("press mouse", () => InputManager.PressButton(MouseButton.Left));
AddAssert("search still not focused", () => !this.ChildrenOfType<ShearedSearchTextBox>().Single().HasFocus);
AddStep("release mouse", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("customisation panel closed by click",
AddStep("move mouse away", () => InputManager.MoveMouseTo(Vector2.Zero));
AddAssert("customisation panel closed",
() => this.ChildrenOfType<ModCustomisationPanel>().Single().ExpandedState.Value,
() => Is.EqualTo(ModCustomisationPanel.ModCustomisationPanelState.Collapsed));

View File

@ -450,7 +450,7 @@ namespace osu.Game.Beatmaps.Formats
// Explicit segments have a new format in which the type is injected into the middle of the control point string.
// To preserve compatibility with osu-stable as much as possible, explicit segments with the same type are converted to use implicit segments by duplicating the control point.
// One exception are consecutive perfect curves, which aren't supported in osu!stable and can lead to decoding issues if encoded as implicit segments
bool needsExplicitSegment = point.Type != lastType || point.Type == PathType.PERFECT_CURVE;
bool needsExplicitSegment = point.Type != lastType || point.Type == PathType.PERFECT_CURVE || i == pathData.Path.ControlPoints.Count - 1;
// Another exception to this is when the last two control points of the last segment were duplicated. This is not a scenario supported by osu!stable.
// Lazer does not add implicit segments for the last two control points of _any_ explicit segment, so an explicit segment is forced in order to maintain consistency with the decoder.

View File

@ -135,11 +135,6 @@ Click to see what's new!", version);
/// </summary>
public static LocalisableString DownloadingUpdate => new TranslatableString(getKey(@"downloading_update"), @"Downloading update...");
/// <summary>
/// "Installing update..."
/// </summary>
public static LocalisableString InstallingUpdate => new TranslatableString(getKey(@"installing_update"), @"Installing update...");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -112,40 +112,10 @@ namespace osu.Game.Overlays.Mods
}, true);
}
protected override bool OnClick(ClickEvent e)
{
if (Enabled.Value)
{
ExpandedState.Value = ExpandedState.Value switch
{
ModCustomisationPanelState.Collapsed => ModCustomisationPanelState.Expanded,
_ => ModCustomisationPanelState.Collapsed
};
}
return base.OnClick(e);
}
private bool touchedThisFrame;
protected override bool OnTouchDown(TouchDownEvent e)
{
if (Enabled.Value)
{
touchedThisFrame = true;
Schedule(() => touchedThisFrame = false);
}
return base.OnTouchDown(e);
}
protected override bool OnHover(HoverEvent e)
{
if (Enabled.Value)
{
if (!touchedThisFrame && panel.ExpandedState.Value == ModCustomisationPanelState.Collapsed)
panel.ExpandedState.Value = ModCustomisationPanelState.ExpandedByHover;
}
if (Enabled.Value && panel.ExpandedState.Value == ModCustomisationPanelState.Collapsed)
panel.ExpandedState.Value = ModCustomisationPanelState.Expanded;
return base.OnHover(e);
}

View File

@ -227,7 +227,7 @@ namespace osu.Game.Overlays.Mods
{
base.Update();
if (ExpandedState.Value == ModCustomisationPanelState.ExpandedByHover
if (ExpandedState.Value == ModCustomisationPanelState.Expanded
&& !ReceivePositionalInputAt(inputManager.CurrentState.Mouse.Position)
&& inputManager.DraggedDrawable == null)
{
@ -239,8 +239,8 @@ namespace osu.Game.Overlays.Mods
public enum ModCustomisationPanelState
{
Collapsed = 0,
ExpandedByHover = 1,
Expanded = 2,
Expanded = 1,
ExpandedByMod = 2,
}
}
}

View File

@ -368,7 +368,7 @@ namespace osu.Game.Overlays.Mods
customisationPanel.Enabled.Value = true;
if (anyModPendingConfiguration)
customisationPanel.ExpandedState.Value = ModCustomisationPanel.ModCustomisationPanelState.Expanded;
customisationPanel.ExpandedState.Value = ModCustomisationPanel.ModCustomisationPanelState.ExpandedByMod;
}
else
{

View File

@ -153,8 +153,22 @@ namespace osu.Game.Overlays
{
base.Update();
float height = toastFlow.Count > 0 ? toastFlow.DrawHeight + 120 : 0;
float alpha = toastFlow.Count > 0 ? MathHelper.Clamp(toastFlow.DrawHeight / 41, 0, 1) * toastFlow.Children.Max(n => n.Alpha) : 0;
float height = 0;
float alpha = 0;
if (toastFlow.Count > 0)
{
float maxNotificationAlpha = 0;
foreach (var t in toastFlow)
{
if (t.Alpha > maxNotificationAlpha)
maxNotificationAlpha = t.Alpha;
}
height = toastFlow.DrawHeight + 120;
alpha = MathHelper.Clamp(toastFlow.DrawHeight / 41, 0, 1) * maxNotificationAlpha;
}
toastContentBackground.Height = (float)Interpolation.DampContinuously(toastContentBackground.Height, height, 10, Clock.ElapsedFrameTime);
toastContentBackground.Alpha = (float)Interpolation.DampContinuously(toastContentBackground.Alpha, alpha, 10, Clock.ElapsedFrameTime);

View File

@ -351,13 +351,19 @@ namespace osu.Game.Rulesets.Objects.Legacy
{
int endPointLength = endPoint == null ? 0 : 1;
if (vertices.Length + endPointLength != 3)
type = PathType.BEZIER;
else if (isLinear(points[0], points[1], endPoint ?? points[2]))
if (FormatVersion < LegacyBeatmapEncoder.FIRST_LAZER_VERSION)
{
// osu-stable special-cased colinear perfect curves to a linear path
type = PathType.LINEAR;
if (vertices.Length + endPointLength != 3)
type = PathType.BEZIER;
else if (isLinear(points[0], points[1], endPoint ?? points[2]))
{
// osu-stable special-cased colinear perfect curves to a linear path
type = PathType.LINEAR;
}
}
else if (vertices.Length + endPointLength > 3)
// Lazer supports perfect curves with less than 3 points and colinear points
type = PathType.BEZIER;
}
// The first control point must have a definite type.

View File

@ -1,14 +1,18 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
@ -16,12 +20,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
public partial class MultiplayerSpectateButton : MultiplayerRoomComposite
{
[Resolved]
private OngoingOperationTracker ongoingOperationTracker { get; set; }
private OngoingOperationTracker ongoingOperationTracker { get; set; } = null!;
[Resolved]
private OsuColour colours { get; set; }
private OsuColour colours { get; set; } = null!;
private IBindable<bool> operationInProgress;
private IBindable<bool> operationInProgress = null!;
private readonly RoundedButton button;
@ -46,10 +50,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
}
[BackgroundDependencyLoader]
private void load()
private void load(OsuConfigManager config)
{
operationInProgress = ongoingOperationTracker.InProgress.GetBoundCopy();
operationInProgress.BindValueChanged(_ => updateState());
automaticallyDownload = config.GetBindable<bool>(OsuSetting.AutomaticallyDownloadMissingBeatmaps);
automaticallyDownload.BindValueChanged(_ => Scheduler.AddOnce(checkForAutomaticDownload));
}
protected override void LoadComplete()
{
base.LoadComplete();
CurrentPlaylistItem.BindValueChanged(_ => Scheduler.AddOnce(checkForAutomaticDownload), true);
}
protected override void OnRoomUpdated()
@ -77,6 +91,64 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
button.Enabled.Value = Client.Room != null
&& Client.Room.State != MultiplayerRoomState.Closed
&& !operationInProgress.Value;
Scheduler.AddOnce(checkForAutomaticDownload);
}
#region Automatic download handling
[Resolved]
private BeatmapLookupCache beatmapLookupCache { get; set; } = null!;
[Resolved]
private BeatmapModelDownloader beatmapDownloader { get; set; } = null!;
[Resolved]
private BeatmapManager beatmaps { get; set; } = null!;
private Bindable<bool> automaticallyDownload = null!;
private CancellationTokenSource? downloadCheckCancellation;
private void checkForAutomaticDownload()
{
PlaylistItem? currentItem = CurrentPlaylistItem.Value;
downloadCheckCancellation?.Cancel();
if (currentItem == null)
return;
if (!automaticallyDownload.Value)
return;
// While we can support automatic downloads when not spectating, there are some usability concerns.
// - In host rotate mode, this could potentially be unwanted by some users (even though they want automatic downloads everywhere else).
// - When first joining a room, the expectation should be that the user is checking out the room, and they may not immediately want to download the selected beatmap.
//
// Rather than over-complicating this flow, let's only auto-download when spectating for the time being.
// A potential path forward would be to have a local auto-download checkbox above the playlist item list area.
if (Client.LocalUser?.State != MultiplayerUserState.Spectating)
return;
// In a perfect world we'd use BeatmapAvailability, but there's no event-driven flow for when a selection changes.
// ie. if selection changes from "not downloaded" to another "not downloaded" we wouldn't get a value changed raised.
beatmapLookupCache
.GetBeatmapAsync(currentItem.Beatmap.OnlineID, (downloadCheckCancellation = new CancellationTokenSource()).Token)
.ContinueWith(resolved => Schedule(() =>
{
var beatmapSet = resolved.GetResultSafely()?.BeatmapSet;
if (beatmapSet == null)
return;
if (beatmaps.IsAvailableLocally(new BeatmapSetInfo { OnlineID = beatmapSet.OnlineID }))
return;
beatmapDownloader.Download(beatmapSet);
}));
}
#endregion
}
}

View File

@ -857,8 +857,9 @@ namespace osu.Game.Screens.Select
// Add those items within the previously found index range that should be displayed.
foreach (var item in toDisplay)
{
var panel = setPool.Get(p => p.Item = item);
var panel = setPool.Get();
panel.Item = item;
panel.Y = item.CarouselYPosition;
Scroll.Add(panel);
@ -900,8 +901,8 @@ namespace osu.Game.Screens.Select
if (item is DrawableCarouselBeatmapSet set)
{
foreach (var diff in set.DrawableBeatmaps)
updateItem(diff, item);
for (int i = 0; i < set.DrawableBeatmaps.Count; i++)
updateItem(set.DrawableBeatmaps[i], item);
}
}
}
@ -1101,7 +1102,7 @@ namespace osu.Game.Screens.Select
}
/// <summary>
/// Update a item's x position and multiplicative alpha based on its y position and
/// Update an item's x position and multiplicative alpha based on its y position and
/// the current scroll position.
/// </summary>
/// <param name="item">The item to be updated.</param>

View File

@ -51,7 +51,7 @@ namespace osu.Game.Screens.Select.Carousel
[Resolved]
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
public IEnumerable<DrawableCarouselItem> DrawableBeatmaps => beatmapContainer?.IsLoaded != true ? Enumerable.Empty<DrawableCarouselItem>() : beatmapContainer.AliveChildren;
public IReadOnlyList<DrawableCarouselItem> DrawableBeatmaps => beatmapContainer?.IsLoaded != true ? Array.Empty<DrawableCarouselItem>() : beatmapContainer;
private Container<DrawableCarouselItem>? beatmapContainer;

View File

@ -4,7 +4,6 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using osu.Framework;
using osu.Framework.Allocation;
@ -16,10 +15,10 @@ using osu.Game.Overlays.Notifications;
namespace osu.Game.Updater
{
/// <summary>
/// An update manager that shows notifications if a newer release is detected.
/// An update manager that shows notifications if a newer release is detected for mobile platforms.
/// Installation is left up to the user.
/// </summary>
public partial class SimpleUpdateManager : UpdateManager
public partial class MobileUpdateNotifier : UpdateManager
{
private string version = null!;
@ -80,19 +79,6 @@ namespace osu.Game.Updater
switch (RuntimeInfo.OS)
{
case RuntimeInfo.Platform.Windows:
bestAsset = release.Assets?.Find(f => f.Name.EndsWith(".exe", StringComparison.Ordinal));
break;
case RuntimeInfo.Platform.macOS:
string arch = RuntimeInformation.OSArchitecture == Architecture.Arm64 ? "Apple.Silicon" : "Intel";
bestAsset = release.Assets?.Find(f => f.Name.EndsWith($".app.{arch}.zip", StringComparison.Ordinal));
break;
case RuntimeInfo.Platform.Linux:
bestAsset = release.Assets?.Find(f => f.Name.EndsWith(".AppImage", StringComparison.Ordinal));
break;
case RuntimeInfo.Platform.iOS:
if (release.Assets?.Exists(f => f.Name.EndsWith(".ipa", StringComparison.Ordinal)) == true)
// iOS releases are available via testflight. this link seems to work well enough for now.

View File

@ -176,12 +176,6 @@ namespace osu.Game.Updater
Text = NotificationsStrings.DownloadingUpdate;
}
public void StartInstall()
{
Progress = 0;
Text = NotificationsStrings.InstallingUpdate;
}
public void FailDownload()
{
State = ProgressNotificationState.Cancelled;

View File

@ -15,7 +15,7 @@ namespace osu.iOS
{
public override Version AssemblyVersion => new Version(NSBundle.MainBundle.InfoDictionary["CFBundleVersion"].ToString());
protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager();
protected override UpdateManager CreateUpdateManager() => new MobileUpdateNotifier();
protected override BatteryInfo CreateBatteryInfo() => new IOSBatteryInfo();

View File

@ -853,6 +853,7 @@ See the LICENCE file in the repository root for full licence text.&#xD;
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpRenamePlacementToArrangementMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002EMemberReordering_002EMigrations_002ECSharpFileLayoutPatternRemoveIsAttributeUpgrade/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EAddAccessorOwnerDeclarationBracesMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EAlwaysTreatStructAsNotReorderableMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002ECSharpPlaceAttributeOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
@ -1060,5 +1061,6 @@ private void load()
<s:Boolean x:Key="/Default/UserDictionary/Words/=Unplayed/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Unproxy/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Unranked/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=velopack/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Welford_0027s/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Zoomable/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>