Merge branch 'master' into fix-multiplayer-unobserved

This commit is contained in:
Dean Herbert
2022-04-06 11:33:10 +09:00
committed by GitHub
68 changed files with 1816 additions and 602 deletions

View File

@ -1,19 +1,16 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays.Settings;
namespace osu.Game.Screens.Edit.Timing
{
internal class TimingSection : Section<TimingControlPoint>
{
private SettingsSlider<double> bpmSlider;
private LabelledTimeSignature timeSignature;
private BPMTextBox bpmTextEntry;
@ -23,7 +20,6 @@ namespace osu.Game.Screens.Edit.Timing
Flow.AddRange(new Drawable[]
{
bpmTextEntry = new BPMTextBox(),
bpmSlider = new BPMSlider(),
timeSignature = new LabelledTimeSignature
{
Label = "Time Signature"
@ -35,11 +31,8 @@ namespace osu.Game.Screens.Edit.Timing
{
if (point.NewValue != null)
{
bpmSlider.Current = point.NewValue.BeatLengthBindable;
bpmSlider.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
bpmTextEntry.Bindable = point.NewValue.BeatLengthBindable;
// no need to hook change handler here as it's the same bindable as above
bpmTextEntry.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
timeSignature.Current = point.NewValue.TimeSignatureBindable;
timeSignature.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
@ -102,51 +95,6 @@ namespace osu.Game.Screens.Edit.Timing
}
}
private class BPMSlider : SettingsSlider<double>
{
private const double sane_minimum = 60;
private const double sane_maximum = 240;
private readonly BindableNumber<double> beatLengthBindable = new TimingControlPoint().BeatLengthBindable;
private readonly BindableDouble bpmBindable = new BindableDouble(60000 / TimingControlPoint.DEFAULT_BEAT_LENGTH)
{
MinValue = sane_minimum,
MaxValue = sane_maximum,
};
public BPMSlider()
{
beatLengthBindable.BindValueChanged(beatLength => updateCurrent(beatLengthToBpm(beatLength.NewValue)), true);
bpmBindable.BindValueChanged(bpm => beatLengthBindable.Value = beatLengthToBpm(bpm.NewValue));
base.Current = bpmBindable;
TransferValueOnCommit = true;
}
public override Bindable<double> Current
{
get => base.Current;
set
{
// incoming will be beat length, not bpm
beatLengthBindable.UnbindBindings();
beatLengthBindable.BindTo(value);
}
}
private void updateCurrent(double newValue)
{
// we use a more sane range for the slider display unless overridden by the user.
// if a value comes in outside our range, we should expand temporarily.
bpmBindable.MinValue = Math.Min(newValue, sane_minimum);
bpmBindable.MaxValue = Math.Max(newValue, sane_maximum);
bpmBindable.Value = newValue;
}
}
private static double beatLengthToBpm(double beatLength) => 60000 / beatLength;
}
}

View File

@ -5,6 +5,8 @@ using System;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Localisation;
using osu.Framework.Threading;
using osu.Game.Graphics;
@ -27,6 +29,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
[CanBeNull]
private MultiplayerRoom room => multiplayerClient.Room;
private Sample countdownTickSample;
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
countdownTickSample = audio.Samples.Get(@"Multiplayer/countdown-tick");
// disabled for now pending further work on sound effect
// countdownTickFinalSample = audio.Samples.Get(@"Multiplayer/countdown-tick-final");
}
protected override void LoadComplete()
{
base.LoadComplete();
@ -36,7 +48,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
}
private MultiplayerCountdown countdown;
private DateTimeOffset countdownChangeTime;
private double countdownChangeTime;
private ScheduledDelegate countdownUpdateDelegate;
private void onRoomUpdated() => Scheduler.AddOnce(() =>
@ -44,20 +56,55 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
if (countdown != room?.Countdown)
{
countdown = room?.Countdown;
countdownChangeTime = DateTimeOffset.Now;
countdownChangeTime = Time.Current;
}
scheduleNextCountdownUpdate();
updateButtonText();
updateButtonColour();
});
private void scheduleNextCountdownUpdate()
{
countdownUpdateDelegate?.Cancel();
if (countdown != null)
countdownUpdateDelegate ??= Scheduler.AddDelayed(updateButtonText, 100, true);
{
// The remaining time on a countdown may be at a fractional portion between two seconds.
// We want to align certain audio/visual cues to the point at which integer seconds change.
// To do so, we schedule to the next whole second. Note that scheduler invocation isn't
// guaranteed to be accurate, so this may still occur slightly late, but even in such a case
// the next invocation will be roughly correct.
double timeToNextSecond = countdownTimeRemaining.TotalMilliseconds % 1000;
countdownUpdateDelegate = Scheduler.AddDelayed(onCountdownTick, timeToNextSecond);
}
else
{
countdownUpdateDelegate?.Cancel();
countdownUpdateDelegate = null;
}
updateButtonText();
updateButtonColour();
});
void onCountdownTick()
{
updateButtonText();
int secondsRemaining = countdownTimeRemaining.Seconds;
playTickSound(secondsRemaining);
if (secondsRemaining > 0)
scheduleNextCountdownUpdate();
}
}
private void playTickSound(int secondsRemaining)
{
if (secondsRemaining < 10) countdownTickSample?.Play();
// disabled for now pending further work on sound effect
// if (secondsRemaining <= 3) countdownTickFinalSample?.Play();
}
private void updateButtonText()
{
@ -75,15 +122,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
if (countdown != null)
{
TimeSpan timeElapsed = DateTimeOffset.Now - countdownChangeTime;
TimeSpan countdownRemaining;
if (timeElapsed > countdown.TimeRemaining)
countdownRemaining = TimeSpan.Zero;
else
countdownRemaining = countdown.TimeRemaining - timeElapsed;
string countdownText = $"Starting in {countdownRemaining:mm\\:ss}";
string countdownText = $"Starting in {countdownTimeRemaining:mm\\:ss}";
switch (localUser?.State)
{
@ -116,6 +155,22 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
}
}
private TimeSpan countdownTimeRemaining
{
get
{
double timeElapsed = Time.Current - countdownChangeTime;
TimeSpan remaining;
if (timeElapsed > countdown.TimeRemaining.TotalMilliseconds)
remaining = TimeSpan.Zero;
else
remaining = countdown.TimeRemaining - TimeSpan.FromMilliseconds(timeElapsed);
return remaining;
}
}
private void updateButtonColour()
{
if (room == null)

View File

@ -117,8 +117,24 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
{
base.PlaylistItemChanged(item);
removeItemFromLists(item.ID);
addItemToLists(item);
var newApiItem = Playlist.SingleOrDefault(i => i.ID == item.ID);
var existingApiItemInQueue = queueList.Items.SingleOrDefault(i => i.ID == item.ID);
// Test if the only change between the two playlist items is the order.
if (newApiItem != null && existingApiItemInQueue != null && existingApiItemInQueue.With(playlistOrder: newApiItem.PlaylistOrder).Equals(newApiItem))
{
// Set the new playlist order directly without refreshing the DrawablePlaylistItem.
existingApiItemInQueue.PlaylistOrder = newApiItem.PlaylistOrder;
// The following isn't really required, but is here for safety and explicitness.
// MultiplayerQueueList internally binds to changes in Playlist to invalidate its own layout, which is mutated on every playlist operation.
queueList.Invalidate();
}
else
{
removeItemFromLists(item.ID);
addItemToLists(item);
}
}
private void addItemToLists(MultiplayerPlaylistItem item)

View File

@ -9,6 +9,7 @@ using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Logging;
using osu.Game.Configuration;
using osu.Game.Extensions;
using osu.Game.Skinning;
@ -84,9 +85,17 @@ namespace osu.Game.Screens.Play.HUD
/// <returns>The new instance.</returns>
public Drawable CreateInstance()
{
Drawable d = (Drawable)Activator.CreateInstance(Type);
d.ApplySkinnableInfo(this);
return d;
try
{
Drawable d = (Drawable)Activator.CreateInstance(Type);
d.ApplySkinnableInfo(this);
return d;
}
catch (Exception e)
{
Logger.Error(e, $"Unable to create skin component {Type.Name}");
return Drawable.Empty();
}
}
}
}

View File

@ -26,7 +26,7 @@ namespace osu.Game.Screens.Select
HeaderText = @"Confirm deletion of";
Buttons = new PopupDialogButton[]
{
new PopupDialogOkButton
new PopupDialogDangerousButton
{
Text = @"Yes. Totally. Delete it.",
Action = () => manager?.Delete(beatmap),

View File

@ -174,7 +174,7 @@ namespace osu.Game.Screens.Select
public virtual bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (e.Action == Hotkey)
if (e.Action == Hotkey && !e.Repeat)
{
TriggerClick();
return true;

View File

@ -38,7 +38,7 @@ namespace osu.Game.Screens.Select
HeaderText = "Confirm deletion of local score";
Buttons = new PopupDialogButton[]
{
new PopupDialogOkButton
new PopupDialogDangerousButton
{
Text = "Yes. Please.",
Action = () => scoreManager?.Delete(score)