mirror of
https://github.com/osukey/osukey.git
synced 2025-08-05 07:33:55 +09:00
Merge branch 'master' into kps
This commit is contained in:
@ -9,6 +9,7 @@ using System.Linq;
|
||||
using System.Reflection;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Extensions.TypeExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
@ -43,12 +44,47 @@ namespace osu.Game.Configuration
|
||||
/// </remarks>
|
||||
public Type? SettingControlType { get; set; }
|
||||
|
||||
public SettingSourceAttribute(Type declaringType, string label, string? description = null)
|
||||
{
|
||||
Label = getLocalisableStringFromMember(label) ?? string.Empty;
|
||||
Description = getLocalisableStringFromMember(description) ?? string.Empty;
|
||||
|
||||
LocalisableString? getLocalisableStringFromMember(string? member)
|
||||
{
|
||||
if (member == null)
|
||||
return null;
|
||||
|
||||
var property = declaringType.GetMember(member, BindingFlags.Static | BindingFlags.Public).FirstOrDefault();
|
||||
|
||||
if (property == null)
|
||||
return null;
|
||||
|
||||
switch (property)
|
||||
{
|
||||
case FieldInfo f:
|
||||
return (LocalisableString)f.GetValue(null).AsNonNull();
|
||||
|
||||
case PropertyInfo p:
|
||||
return (LocalisableString)p.GetValue(null).AsNonNull();
|
||||
|
||||
default:
|
||||
throw new InvalidOperationException($"Member \"{member}\" was not found in type {declaringType} (must be a static field or property)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public SettingSourceAttribute(string? label, string? description = null)
|
||||
{
|
||||
Label = label ?? string.Empty;
|
||||
Description = description ?? string.Empty;
|
||||
}
|
||||
|
||||
public SettingSourceAttribute(Type declaringType, string label, string description, int orderPosition)
|
||||
: this(declaringType, label, description)
|
||||
{
|
||||
OrderPosition = orderPosition;
|
||||
}
|
||||
|
||||
public SettingSourceAttribute(string label, string description, int orderPosition)
|
||||
: this(label, description)
|
||||
{
|
||||
|
@ -128,5 +128,13 @@ namespace osu.Game.Online.API
|
||||
IBindable<UserActivity> IAPIProvider.Activity => Activity;
|
||||
|
||||
public void FailNextLogin() => shouldFailNextLogin = true;
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
// Ensure (as much as we can) that any pending tasks are run.
|
||||
Scheduler.Update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,8 @@ using osuTK;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.UI;
|
||||
@ -126,7 +128,7 @@ namespace osu.Game.Online.Leaderboards
|
||||
|
||||
private class HitResultCell : CompositeDrawable
|
||||
{
|
||||
private readonly string displayName;
|
||||
private readonly LocalisableString displayName;
|
||||
private readonly HitResult result;
|
||||
private readonly int count;
|
||||
|
||||
@ -134,7 +136,7 @@ namespace osu.Game.Online.Leaderboards
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
displayName = stat.DisplayName;
|
||||
displayName = stat.DisplayName.ToUpper();
|
||||
result = stat.Result;
|
||||
count = stat.Count;
|
||||
}
|
||||
@ -153,7 +155,7 @@ namespace osu.Game.Online.Leaderboards
|
||||
new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold),
|
||||
Text = displayName.ToUpperInvariant(),
|
||||
Text = displayName.ToUpper(),
|
||||
Colour = colours.ForHitResult(result),
|
||||
},
|
||||
new OsuSpriteText
|
||||
|
@ -59,7 +59,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
|
||||
/// <summary>
|
||||
/// The statistics that appear in the table, in order of appearance.
|
||||
/// </summary>
|
||||
private readonly List<(HitResult result, string displayName)> statisticResultTypes = new List<(HitResult, string)>();
|
||||
private readonly List<(HitResult result, LocalisableString displayName)> statisticResultTypes = new List<(HitResult, LocalisableString)>();
|
||||
|
||||
private bool showPerformancePoints;
|
||||
|
||||
@ -114,7 +114,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
|
||||
if (result.IsBonus())
|
||||
continue;
|
||||
|
||||
string displayName = ruleset.GetDisplayNameForHitResult(result);
|
||||
var displayName = ruleset.GetDisplayNameForHitResult(result);
|
||||
|
||||
columns.Add(new TableColumn(displayName, Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 35, maxSize: 60)));
|
||||
statisticResultTypes.Add((result, displayName));
|
||||
|
@ -31,10 +31,7 @@ namespace osu.Game.Overlays.Mods
|
||||
set => current.Current = value;
|
||||
}
|
||||
|
||||
private readonly BindableNumberWithCurrent<double> current = new BindableNumberWithCurrent<double>(1)
|
||||
{
|
||||
Precision = 0.01
|
||||
};
|
||||
private readonly BindableNumberWithCurrent<double> current = new BindableNumberWithCurrent<double>(1);
|
||||
|
||||
private readonly Box underlayBackground;
|
||||
private readonly Box contentBackground;
|
||||
|
@ -66,7 +66,10 @@ namespace osu.Game.Overlays.Mods
|
||||
private IModHotkeyHandler hotkeyHandler = null!;
|
||||
|
||||
private Task? latestLoadTask;
|
||||
internal bool ItemsLoaded => latestLoadTask?.IsCompleted == true;
|
||||
private ICollection<ModPanel>? latestLoadedPanels;
|
||||
internal bool ItemsLoaded => latestLoadTask?.IsCompleted == true && latestLoadedPanels?.All(panel => panel.Parent != null) == true;
|
||||
|
||||
public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks;
|
||||
|
||||
public ModColumn(ModType modType, bool allowIncompatibleSelection)
|
||||
{
|
||||
@ -130,7 +133,8 @@ namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
cancellationTokenSource?.Cancel();
|
||||
|
||||
var panels = availableMods.Select(mod => CreateModPanel(mod).With(panel => panel.Shear = Vector2.Zero));
|
||||
var panels = availableMods.Select(mod => CreateModPanel(mod).With(panel => panel.Shear = Vector2.Zero)).ToArray();
|
||||
latestLoadedPanels = panels;
|
||||
|
||||
latestLoadTask = LoadComponentsAsync(panels, loaded =>
|
||||
{
|
||||
|
@ -57,6 +57,18 @@ namespace osu.Game.Overlays.Mods
|
||||
Filtered.BindValueChanged(_ => updateFilterState(), true);
|
||||
}
|
||||
|
||||
protected override void Select()
|
||||
{
|
||||
modState.PendingConfiguration = Mod.RequiresConfiguration;
|
||||
Active.Value = true;
|
||||
}
|
||||
|
||||
protected override void Deselect()
|
||||
{
|
||||
modState.PendingConfiguration = false;
|
||||
Active.Value = false;
|
||||
}
|
||||
|
||||
#region Filtering support
|
||||
|
||||
private void updateFilterState()
|
||||
|
@ -37,8 +37,6 @@ namespace osu.Game.Overlays.Mods
|
||||
|
||||
Title = preset.Value.Name;
|
||||
Description = preset.Value.Description;
|
||||
|
||||
Action = toggleRequestedByUser;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@ -54,15 +52,19 @@ namespace osu.Game.Overlays.Mods
|
||||
selectedMods.BindValueChanged(_ => selectedModsChanged(), true);
|
||||
}
|
||||
|
||||
private void toggleRequestedByUser()
|
||||
protected override void Select()
|
||||
{
|
||||
// if the preset is not active at the point of the user click, then set the mods using the preset directly, discarding any previous selections,
|
||||
// which will also have the side effect of activating the preset (see `updateActiveState()`).
|
||||
selectedMods.Value = Preset.Value.Mods.ToArray();
|
||||
}
|
||||
|
||||
protected override void Deselect()
|
||||
{
|
||||
// if the preset is not active at the point of the user click, then set the mods using the preset directly, discarding any previous selections.
|
||||
// if the preset is active when the user has clicked it, then it means that the set of active mods is exactly equal to the set of mods in the preset
|
||||
// (there are no other active mods than what the preset specifies, and the mod settings match exactly).
|
||||
// therefore it's safe to just clear selected mods, since it will have the effect of toggling the preset off.
|
||||
selectedMods.Value = !Active.Value
|
||||
? Preset.Value.Mods.ToArray()
|
||||
: Array.Empty<Mod>();
|
||||
selectedMods.Value = Array.Empty<Mod>();
|
||||
}
|
||||
|
||||
private void selectedModsChanged()
|
||||
|
@ -159,12 +159,15 @@ namespace osu.Game.Overlays.Mods
|
||||
|
||||
int wordIndex = 0;
|
||||
|
||||
headerText.AddText(text, t =>
|
||||
ITextPart part = headerText.AddText(text, t =>
|
||||
{
|
||||
if (wordIndex == 0)
|
||||
t.Font = t.Font.With(weight: FontWeight.SemiBold);
|
||||
wordIndex += 1;
|
||||
});
|
||||
|
||||
// Reset the index so that if the parts are refreshed (e.g. through changes in localisation) the correct word is re-emboldened.
|
||||
part.DrawablePartsRecreated += _ => wordIndex = 0;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
|
@ -87,7 +87,7 @@ namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
if (AllowCustomisation)
|
||||
{
|
||||
yield return customisationButton = new ShearedToggleButton(BUTTON_WIDTH)
|
||||
yield return CustomisationButton = new ShearedToggleButton(BUTTON_WIDTH)
|
||||
{
|
||||
Text = ModSelectOverlayStrings.ModCustomisation,
|
||||
Active = { BindTarget = customisationVisible }
|
||||
@ -107,11 +107,11 @@ namespace osu.Game.Overlays.Mods
|
||||
private ColumnScrollContainer columnScroll = null!;
|
||||
private ColumnFlowContainer columnFlow = null!;
|
||||
private FillFlowContainer<ShearedButton> footerButtonFlow = null!;
|
||||
private ShearedButton backButton = null!;
|
||||
|
||||
private DifficultyMultiplierDisplay? multiplierDisplay;
|
||||
|
||||
private ShearedToggleButton? customisationButton;
|
||||
protected ShearedButton BackButton { get; private set; } = null!;
|
||||
protected ShearedToggleButton? CustomisationButton { get; private set; }
|
||||
|
||||
private Sample? columnAppearSample;
|
||||
|
||||
@ -214,7 +214,7 @@ namespace osu.Game.Overlays.Mods
|
||||
Horizontal = 70
|
||||
},
|
||||
Spacing = new Vector2(10),
|
||||
ChildrenEnumerable = CreateFooterButtons().Prepend(backButton = new ShearedButton(BUTTON_WIDTH)
|
||||
ChildrenEnumerable = CreateFooterButtons().Prepend(BackButton = new ShearedButton(BUTTON_WIDTH)
|
||||
{
|
||||
Text = CommonStrings.Back,
|
||||
Action = Hide,
|
||||
@ -247,8 +247,8 @@ namespace osu.Game.Overlays.Mods
|
||||
modSettingChangeTracker?.Dispose();
|
||||
|
||||
updateMultiplier();
|
||||
updateCustomisation(val);
|
||||
updateFromExternalSelection();
|
||||
updateCustomisation();
|
||||
|
||||
if (AllowCustomisation)
|
||||
{
|
||||
@ -356,25 +356,26 @@ namespace osu.Game.Overlays.Mods
|
||||
multiplierDisplay.Current.Value = multiplier;
|
||||
}
|
||||
|
||||
private void updateCustomisation(ValueChangedEvent<IReadOnlyList<Mod>> valueChangedEvent)
|
||||
private void updateCustomisation()
|
||||
{
|
||||
if (customisationButton == null)
|
||||
if (CustomisationButton == null)
|
||||
return;
|
||||
|
||||
bool anyCustomisableMod = false;
|
||||
bool anyModWithRequiredCustomisationAdded = false;
|
||||
bool anyCustomisableModActive = false;
|
||||
bool anyModPendingConfiguration = false;
|
||||
|
||||
foreach (var mod in SelectedMods.Value)
|
||||
foreach (var modState in allAvailableMods)
|
||||
{
|
||||
anyCustomisableMod |= mod.GetSettingsSourceProperties().Any();
|
||||
anyModWithRequiredCustomisationAdded |= valueChangedEvent.OldValue.All(m => m.GetType() != mod.GetType()) && mod.RequiresConfiguration;
|
||||
anyCustomisableModActive |= modState.Active.Value && modState.Mod.GetSettingsSourceProperties().Any();
|
||||
anyModPendingConfiguration |= modState.PendingConfiguration;
|
||||
modState.PendingConfiguration = false;
|
||||
}
|
||||
|
||||
if (anyCustomisableMod)
|
||||
if (anyCustomisableModActive)
|
||||
{
|
||||
customisationVisible.Disabled = false;
|
||||
|
||||
if (anyModWithRequiredCustomisationAdded && !customisationVisible.Value)
|
||||
if (anyModPendingConfiguration && !customisationVisible.Value)
|
||||
customisationVisible.Value = true;
|
||||
}
|
||||
else
|
||||
@ -394,7 +395,7 @@ namespace osu.Game.Overlays.Mods
|
||||
|
||||
foreach (var button in footerButtonFlow)
|
||||
{
|
||||
if (button != customisationButton)
|
||||
if (button != CustomisationButton)
|
||||
button.Enabled.Value = !customisationVisible.Value;
|
||||
}
|
||||
|
||||
@ -587,14 +588,14 @@ namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
if (customisationVisible.Value)
|
||||
{
|
||||
Debug.Assert(customisationButton != null);
|
||||
customisationButton.TriggerClick();
|
||||
Debug.Assert(CustomisationButton != null);
|
||||
CustomisationButton.TriggerClick();
|
||||
|
||||
if (!immediate)
|
||||
return;
|
||||
}
|
||||
|
||||
backButton.TriggerClick();
|
||||
BackButton.TriggerClick();
|
||||
}
|
||||
}
|
||||
|
||||
@ -708,7 +709,18 @@ namespace osu.Game.Overlays.Mods
|
||||
FinishTransforms();
|
||||
}
|
||||
|
||||
protected override bool RequiresChildrenUpdate => base.RequiresChildrenUpdate || (Column as ModColumn)?.SelectionAnimationRunning == true;
|
||||
protected override bool RequiresChildrenUpdate
|
||||
{
|
||||
get
|
||||
{
|
||||
bool result = base.RequiresChildrenUpdate;
|
||||
|
||||
if (Column is ModColumn modColumn)
|
||||
result |= !modColumn.ItemsLoaded || modColumn.SelectionAnimationRunning;
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
|
@ -143,9 +143,25 @@ namespace osu.Game.Overlays.Mods
|
||||
}
|
||||
};
|
||||
|
||||
Action = () => Active.Toggle();
|
||||
Action = () =>
|
||||
{
|
||||
if (!Active.Value)
|
||||
Select();
|
||||
else
|
||||
Deselect();
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs all actions necessary to select this <see cref="ModSelectPanel"/>.
|
||||
/// </summary>
|
||||
protected abstract void Select();
|
||||
|
||||
/// <summary>
|
||||
/// Performs all actions necessary to deselect this <see cref="ModSelectPanel"/>.
|
||||
/// </summary>
|
||||
protected abstract void Deselect();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(AudioManager audio, ISamplePlaybackDisabler? samplePlaybackDisabler)
|
||||
{
|
||||
|
@ -24,6 +24,13 @@ namespace osu.Game.Overlays.Mods
|
||||
/// </summary>
|
||||
public BindableBool Active { get; } = new BindableBool();
|
||||
|
||||
/// <summary>
|
||||
/// Whether the mod requires further customisation.
|
||||
/// This flag is read by the <see cref="ModSelectOverlay"/> to determine if the customisation panel should be opened after a mod change
|
||||
/// and cleared after reading.
|
||||
/// </summary>
|
||||
public bool PendingConfiguration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the mod is currently filtered out due to not matching imposed criteria.
|
||||
/// </summary>
|
||||
|
@ -14,6 +14,7 @@ using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
@ -70,85 +71,90 @@ namespace osu.Game.Overlays.Profile.Header
|
||||
Masking = true,
|
||||
CornerRadius = avatar_size * 0.25f,
|
||||
},
|
||||
new Container
|
||||
new OsuContextMenuContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
AutoSizeAxes = Axes.X,
|
||||
Padding = new MarginPadding { Left = 10 },
|
||||
Children = new Drawable[]
|
||||
Child = new Container
|
||||
{
|
||||
new FillFlowContainer
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
AutoSizeAxes = Axes.X,
|
||||
Padding = new MarginPadding { Left = 10 },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new Drawable[]
|
||||
new FillFlowContainer
|
||||
{
|
||||
new FillFlowContainer
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Children = new Drawable[]
|
||||
new FillFlowContainer
|
||||
{
|
||||
usernameText = new OsuSpriteText
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Font = OsuFont.GetFont(size: 24, weight: FontWeight.Regular)
|
||||
},
|
||||
openUserExternally = new ExternalLinkButton
|
||||
{
|
||||
Margin = new MarginPadding { Left = 5 },
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
},
|
||||
}
|
||||
},
|
||||
titleText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.GetFont(size: 18, weight: FontWeight.Regular)
|
||||
},
|
||||
}
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
Origin = Anchor.BottomLeft,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Direction = FillDirection.Vertical,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
supporterTag = new SupporterIcon
|
||||
{
|
||||
Height = 20,
|
||||
Margin = new MarginPadding { Top = 5 }
|
||||
},
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 1.5f,
|
||||
Margin = new MarginPadding { Top = 10 },
|
||||
Colour = colourProvider.Light1,
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Margin = new MarginPadding { Top = 5 },
|
||||
Direction = FillDirection.Horizontal,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
userFlag = new UpdateableFlag
|
||||
{
|
||||
Size = new Vector2(28, 20),
|
||||
ShowPlaceholderOnUnknown = false,
|
||||
},
|
||||
userCountryText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.GetFont(size: 17.5f, weight: FontWeight.Regular),
|
||||
Margin = new MarginPadding { Left = 10 },
|
||||
Origin = Anchor.CentreLeft,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Colour = colourProvider.Light1,
|
||||
usernameText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.GetFont(size: 24, weight: FontWeight.Regular)
|
||||
},
|
||||
openUserExternally = new ExternalLinkButton
|
||||
{
|
||||
Margin = new MarginPadding { Left = 5 },
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
titleText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.GetFont(size: 18, weight: FontWeight.Regular)
|
||||
},
|
||||
}
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
Origin = Anchor.BottomLeft,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Direction = FillDirection.Vertical,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
supporterTag = new SupporterIcon
|
||||
{
|
||||
Height = 20,
|
||||
Margin = new MarginPadding { Top = 5 }
|
||||
},
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 1.5f,
|
||||
Margin = new MarginPadding { Top = 10 },
|
||||
Colour = colourProvider.Light1,
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Margin = new MarginPadding { Top = 5 },
|
||||
Direction = FillDirection.Horizontal,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
userFlag = new UpdateableFlag
|
||||
{
|
||||
Size = new Vector2(28, 20),
|
||||
ShowPlaceholderOnUnknown = false,
|
||||
},
|
||||
userCountryText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.GetFont(size: 17.5f, weight: FontWeight.Regular),
|
||||
Margin = new MarginPadding { Left = 10 },
|
||||
Origin = Anchor.CentreLeft,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Colour = colourProvider.Light1,
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Rulesets;
|
||||
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
@ -21,7 +22,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
/// <summary>
|
||||
/// The user readable description of this mod.
|
||||
/// </summary>
|
||||
string Description { get; }
|
||||
LocalisableString Description { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of this mod.
|
||||
|
@ -9,6 +9,7 @@ using Newtonsoft.Json;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.TypeExtensions;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.UI;
|
||||
@ -34,7 +35,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
public virtual ModType Type => ModType.Fun;
|
||||
|
||||
[JsonIgnore]
|
||||
public abstract string Description { get; }
|
||||
public abstract LocalisableString Description { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The tooltip to display for this mod when used in a <see cref="ModIcon"/>.
|
||||
|
@ -6,6 +6,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
@ -23,7 +24,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
|
||||
public override string Acronym => "AS";
|
||||
|
||||
public override string Description => "Let track speed adapt to you.";
|
||||
public override LocalisableString Description => "Let track speed adapt to you.";
|
||||
|
||||
public override ModType Type => ModType.Fun;
|
||||
|
||||
|
@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Replays;
|
||||
@ -17,7 +18,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override string Acronym => "AT";
|
||||
public override IconUsage? Icon => OsuIcon.ModAuto;
|
||||
public override ModType Type => ModType.Automation;
|
||||
public override string Description => "Watch a perfect automated play through the song.";
|
||||
public override LocalisableString Description => "Watch a perfect automated play through the song.";
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
public bool PerformFail() => false;
|
||||
|
@ -5,6 +5,7 @@ using System;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.UI;
|
||||
@ -34,7 +35,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
|
||||
public override string Name => "Barrel Roll";
|
||||
public override string Acronym => "BR";
|
||||
public override string Description => "The whole playfield is on a wheel!";
|
||||
public override LocalisableString Description => "The whole playfield is on a wheel!";
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
public override string SettingDescription => $"{SpinSpeed.Value:N2} rpm {Direction.Value.GetDescription().ToLowerInvariant()}";
|
||||
|
@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.UI;
|
||||
@ -27,7 +28,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override string Name => "Cinema";
|
||||
public override string Acronym => "CN";
|
||||
public override IconUsage? Icon => OsuIcon.ModCinema;
|
||||
public override string Description => "Watch the video without visual distractions.";
|
||||
public override LocalisableString Description => "Watch the video without visual distractions.";
|
||||
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModAutoplay)).ToArray();
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
@ -15,7 +16,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
|
||||
public override IconUsage? Icon => FontAwesome.Solid.History;
|
||||
|
||||
public override string Description => "Feeling nostalgic?";
|
||||
public override LocalisableString Description => "Feeling nostalgic?";
|
||||
|
||||
public override ModType Type => ModType.Conversion;
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
@ -12,7 +13,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override string Name => "Daycore";
|
||||
public override string Acronym => "DC";
|
||||
public override IconUsage? Icon => null;
|
||||
public override string Description => "Whoaaaaa...";
|
||||
public override LocalisableString Description => "Whoaaaaa...";
|
||||
|
||||
private readonly BindableNumber<double> tempoAdjust = new BindableDouble(1);
|
||||
private readonly BindableNumber<double> freqAdjust = new BindableDouble(1);
|
||||
|
@ -5,6 +5,7 @@ using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
|
||||
@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
public override string Name => @"Difficulty Adjust";
|
||||
|
||||
public override string Description => @"Override a beatmap's difficulty settings.";
|
||||
public override LocalisableString Description => @"Override a beatmap's difficulty settings.";
|
||||
|
||||
public override string Acronym => "DA";
|
||||
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
|
||||
@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override string Acronym => "DT";
|
||||
public override IconUsage? Icon => OsuIcon.ModDoubleTime;
|
||||
public override ModType Type => ModType.DifficultyIncrease;
|
||||
public override string Description => "Zoooooooooom...";
|
||||
public override LocalisableString Description => "Zoooooooooom...";
|
||||
|
||||
[SettingSource("Speed increase", "The actual increase to apply")]
|
||||
public override BindableNumber<double> SpeedChange { get; } = new BindableDouble
|
||||
|
@ -11,6 +11,7 @@ using osu.Framework.Graphics.Rendering;
|
||||
using osu.Framework.Graphics.Rendering.Vertices;
|
||||
using osu.Framework.Graphics.Shaders;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps.Timing;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
@ -30,7 +31,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override string Acronym => "FL";
|
||||
public override IconUsage? Icon => OsuIcon.ModFlashlight;
|
||||
public override ModType Type => ModType.DifficultyIncrease;
|
||||
public override string Description => "Restricted view area.";
|
||||
public override LocalisableString Description => "Restricted view area.";
|
||||
|
||||
[SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")]
|
||||
public abstract BindableFloat SizeMultiplier { get; }
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
|
||||
@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override string Acronym => "HT";
|
||||
public override IconUsage? Icon => OsuIcon.ModHalftime;
|
||||
public override ModType Type => ModType.DifficultyReduction;
|
||||
public override string Description => "Less zoom...";
|
||||
public override LocalisableString Description => "Less zoom...";
|
||||
|
||||
[SettingSource("Speed decrease", "The actual decrease to apply")]
|
||||
public override BindableNumber<double> SpeedChange { get; } = new BindableDouble
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
|
||||
@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override string Acronym => "HR";
|
||||
public override IconUsage? Icon => OsuIcon.ModHardRock;
|
||||
public override ModType Type => ModType.DifficultyIncrease;
|
||||
public override string Description => "Everything just got a bit harder...";
|
||||
public override LocalisableString Description => "Everything just got a bit harder...";
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModEasy), typeof(ModDifficultyAdjust) };
|
||||
|
||||
public void ReadFromDifficulty(IBeatmapDifficultyInfo difficulty)
|
||||
|
@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override string Name => "Muted";
|
||||
public override string Acronym => "MU";
|
||||
public override IconUsage? Icon => FontAwesome.Solid.VolumeMute;
|
||||
public override string Description => "Can you still feel the rhythm without music?";
|
||||
public override LocalisableString Description => "Can you still feel the rhythm without music?";
|
||||
public override ModType Type => ModType.Fun;
|
||||
public override double ScoreMultiplier => 1;
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Beatmaps.Timing;
|
||||
@ -23,7 +24,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override string Name => "Nightcore";
|
||||
public override string Acronym => "NC";
|
||||
public override IconUsage? Icon => OsuIcon.ModNightcore;
|
||||
public override string Description => "Uguuuuuuuu...";
|
||||
public override LocalisableString Description => "Uguuuuuuuu...";
|
||||
}
|
||||
|
||||
public abstract class ModNightcore<TObject> : ModNightcore, IApplicableToDrawableRuleset<TObject>
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
@ -13,7 +14,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override string Acronym => "NF";
|
||||
public override IconUsage? Icon => OsuIcon.ModNoFail;
|
||||
public override ModType Type => ModType.DifficultyReduction;
|
||||
public override string Description => "You can't fail, no matter what.";
|
||||
public override LocalisableString Description => "You can't fail, no matter what.";
|
||||
public override double ScoreMultiplier => 0.5;
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModFailCondition), typeof(ModAutoplay) };
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
@ -12,7 +13,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
public override string Name => "No Mod";
|
||||
public override string Acronym => "NM";
|
||||
public override string Description => "No mods applied.";
|
||||
public override LocalisableString Description => "No mods applied.";
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override IconUsage? Icon => FontAwesome.Solid.Ban;
|
||||
public override ModType Type => ModType.System;
|
||||
|
@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
@ -17,7 +18,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override IconUsage? Icon => OsuIcon.ModPerfect;
|
||||
public override ModType Type => ModType.DifficultyIncrease;
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override string Description => "SS or quit.";
|
||||
public override LocalisableString Description => "SS or quit.";
|
||||
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModSuddenDeath)).ToArray();
|
||||
|
||||
|
@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
@ -16,7 +17,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override string Acronym => "SD";
|
||||
public override IconUsage? Icon => OsuIcon.ModSuddenDeath;
|
||||
public override ModType Type => ModType.DifficultyIncrease;
|
||||
public override string Description => "Miss and fail.";
|
||||
public override LocalisableString Description => "Miss and fail.";
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModPerfect)).ToArray();
|
||||
|
@ -5,6 +5,7 @@ using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
@ -13,7 +14,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
public override string Name => "Wind Down";
|
||||
public override string Acronym => "WD";
|
||||
public override string Description => "Sloooow doooown...";
|
||||
public override LocalisableString Description => "Sloooow doooown...";
|
||||
public override IconUsage? Icon => FontAwesome.Solid.ChevronCircleDown;
|
||||
public override double ScoreMultiplier => 1.0;
|
||||
|
||||
|
@ -5,6 +5,7 @@ using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
@ -13,7 +14,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
public override string Name => "Wind Up";
|
||||
public override string Acronym => "WU";
|
||||
public override string Description => "Can you keep up?";
|
||||
public override LocalisableString Description => "Can you keep up?";
|
||||
public override IconUsage? Icon => FontAwesome.Solid.ChevronCircleUp;
|
||||
public override double ScoreMultiplier => 1.0;
|
||||
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
@ -10,7 +11,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
public override string Name => string.Empty;
|
||||
public override string Acronym => string.Empty;
|
||||
public override string Description => string.Empty;
|
||||
public override LocalisableString Description => string.Empty;
|
||||
public override double ScoreMultiplier => 0;
|
||||
|
||||
public Mod[] Mods { get; }
|
||||
|
@ -1,6 +1,8 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
public class UnknownMod : Mod
|
||||
@ -12,7 +14,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
|
||||
public override string Name => $"Unknown mod ({OriginalAcronym})";
|
||||
public override string Acronym => $"{OriginalAcronym}??";
|
||||
public override string Description => "This mod could not be resolved by the game.";
|
||||
public override LocalisableString Description => "This mod could not be resolved by the game.";
|
||||
public override double ScoreMultiplier => 0;
|
||||
|
||||
public override bool UserPlayable => false;
|
||||
|
@ -5,8 +5,8 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
|
||||
namespace osu.Game.Rulesets
|
||||
@ -68,8 +68,14 @@ namespace osu.Game.Rulesets
|
||||
{
|
||||
try
|
||||
{
|
||||
var resolvedType = Type.GetType(r.InstantiationInfo)
|
||||
?? throw new RulesetLoadException(@"Type could not be resolved");
|
||||
var resolvedType = Type.GetType(r.InstantiationInfo);
|
||||
|
||||
if (resolvedType == null)
|
||||
{
|
||||
// ruleset DLL was probably deleted.
|
||||
r.Available = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
var instanceInfo = (Activator.CreateInstance(resolvedType) as Ruleset)?.RulesetInfo
|
||||
?? throw new RulesetLoadException(@"Instantiation failure");
|
||||
@ -83,17 +89,35 @@ namespace osu.Game.Rulesets
|
||||
r.InstantiationInfo = instanceInfo.InstantiationInfo;
|
||||
r.Available = true;
|
||||
|
||||
testRulesetCompatibility(r);
|
||||
|
||||
detachedRulesets.Add(r.Clone());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
r.Available = false;
|
||||
Logger.Log($"Could not load ruleset {r}: {ex.Message}");
|
||||
LogFailedLoad(r.Name, ex);
|
||||
}
|
||||
}
|
||||
|
||||
availableRulesets.AddRange(detachedRulesets.OrderBy(r => r));
|
||||
});
|
||||
}
|
||||
|
||||
private void testRulesetCompatibility(RulesetInfo rulesetInfo)
|
||||
{
|
||||
// do various operations to ensure that we are in a good state.
|
||||
// if we can avoid loading the ruleset at this point (rather than erroring later in runtime) then that is preferred.
|
||||
var instance = rulesetInfo.CreateInstance();
|
||||
|
||||
instance.CreateAllMods();
|
||||
instance.CreateIcon();
|
||||
instance.CreateResourceStore();
|
||||
|
||||
var beatmap = new Beatmap();
|
||||
var converter = instance.CreateBeatmapConverter(beatmap);
|
||||
|
||||
instance.CreateBeatmapProcessor(converter.Convert());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ using osu.Game.Users;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Rulesets.Filter;
|
||||
@ -288,7 +289,7 @@ namespace osu.Game.Rulesets
|
||||
/// </summary>
|
||||
/// <param name="variant">The variant.</param>
|
||||
/// <returns>A descriptive name of the variant.</returns>
|
||||
public virtual string GetVariantName(int variant) => string.Empty;
|
||||
public virtual LocalisableString GetVariantName(int variant) => string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// For rulesets which support legacy (osu-stable) replay conversion, this method will create an empty replay frame
|
||||
@ -313,7 +314,7 @@ namespace osu.Game.Rulesets
|
||||
/// <returns>
|
||||
/// All valid <see cref="HitResult"/>s along with a display-friendly name.
|
||||
/// </returns>
|
||||
public IEnumerable<(HitResult result, string displayName)> GetHitResults()
|
||||
public IEnumerable<(HitResult result, LocalisableString displayName)> GetHitResults()
|
||||
{
|
||||
var validResults = GetValidHitResults();
|
||||
|
||||
@ -351,7 +352,7 @@ namespace osu.Game.Rulesets
|
||||
/// </summary>
|
||||
/// <param name="result">The result type to get the name for.</param>
|
||||
/// <returns>The display name.</returns>
|
||||
public virtual string GetDisplayNameForHitResult(HitResult result) => result.GetDescription();
|
||||
public virtual LocalisableString GetDisplayNameForHitResult(HitResult result) => result.GetLocalisableDescription();
|
||||
|
||||
/// <summary>
|
||||
/// Creates ruleset-specific beatmap filter criteria to be used on the song select screen.
|
||||
|
@ -138,7 +138,7 @@ namespace osu.Game.Rulesets
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(e, $"Failed to load ruleset {filename}");
|
||||
LogFailedLoad(filename, e);
|
||||
}
|
||||
}
|
||||
|
||||
@ -158,7 +158,7 @@ namespace osu.Game.Rulesets
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(e, $"Failed to add ruleset {assembly}");
|
||||
LogFailedLoad(assembly.FullName, e);
|
||||
}
|
||||
}
|
||||
|
||||
@ -173,6 +173,12 @@ namespace osu.Game.Rulesets
|
||||
AppDomain.CurrentDomain.AssemblyResolve -= resolveRulesetDependencyAssembly;
|
||||
}
|
||||
|
||||
protected void LogFailedLoad(string name, Exception exception)
|
||||
{
|
||||
Logger.Log($"Could not load ruleset {name}. Please check for an update from the developer.", level: LogLevel.Error);
|
||||
Logger.Log($"Ruleset load failed: {exception}");
|
||||
}
|
||||
|
||||
#region Implementation of IRulesetStore
|
||||
|
||||
IRulesetInfo? IRulesetStore.GetRuleset(int id) => GetRuleset(id);
|
||||
|
@ -86,7 +86,7 @@ namespace osu.Game.Rulesets.UI
|
||||
this.gameplayStartTime = gameplayStartTime;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(IGameplayClock? gameplayClock)
|
||||
{
|
||||
if (gameplayClock != null)
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Scoring
|
||||
@ -30,9 +31,9 @@ namespace osu.Game.Scoring
|
||||
/// <summary>
|
||||
/// A custom display name for the result type. May be provided by rulesets to give better clarity.
|
||||
/// </summary>
|
||||
public string DisplayName { get; }
|
||||
public LocalisableString DisplayName { get; }
|
||||
|
||||
public HitResultDisplayStatistic(HitResult result, int count, int? maxCount, string displayName)
|
||||
public HitResultDisplayStatistic(HitResult result, int count, int? maxCount, LocalisableString displayName)
|
||||
{
|
||||
Result = result;
|
||||
Count = count;
|
||||
|
@ -1,96 +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 osu.Framework.Bindables;
|
||||
using osu.Framework.Timing;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="ISpectatorPlayerClock"/> which catches up using rate adjustment.
|
||||
/// </summary>
|
||||
public class CatchUpSpectatorPlayerClock : ISpectatorPlayerClock
|
||||
{
|
||||
/// <summary>
|
||||
/// The catch up rate.
|
||||
/// </summary>
|
||||
public const double CATCHUP_RATE = 2;
|
||||
|
||||
/// <summary>
|
||||
/// The source clock.
|
||||
/// </summary>
|
||||
public IFrameBasedClock? Source { get; set; }
|
||||
|
||||
public double CurrentTime { get; private set; }
|
||||
|
||||
public bool IsRunning { get; private set; }
|
||||
|
||||
public void Reset() => CurrentTime = 0;
|
||||
|
||||
public void Start() => IsRunning = true;
|
||||
|
||||
public void Stop() => IsRunning = false;
|
||||
|
||||
void IAdjustableClock.Start()
|
||||
{
|
||||
// Our running state should only be managed by an ISyncManager, ignore calls from external sources.
|
||||
}
|
||||
|
||||
void IAdjustableClock.Stop()
|
||||
{
|
||||
// Our running state should only be managed by an ISyncManager, ignore calls from external sources.
|
||||
}
|
||||
|
||||
public bool Seek(double position)
|
||||
{
|
||||
CurrentTime = position;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void ResetSpeedAdjustments()
|
||||
{
|
||||
}
|
||||
|
||||
public double Rate => IsCatchingUp ? CATCHUP_RATE : 1;
|
||||
|
||||
double IAdjustableClock.Rate
|
||||
{
|
||||
get => Rate;
|
||||
set => throw new NotSupportedException();
|
||||
}
|
||||
|
||||
double IClock.Rate => Rate;
|
||||
|
||||
public void ProcessFrame()
|
||||
{
|
||||
ElapsedFrameTime = 0;
|
||||
FramesPerSecond = 0;
|
||||
|
||||
if (Source == null)
|
||||
return;
|
||||
|
||||
Source.ProcessFrame();
|
||||
|
||||
if (IsRunning)
|
||||
{
|
||||
double elapsedSource = Source.ElapsedFrameTime;
|
||||
double elapsed = elapsedSource * Rate;
|
||||
|
||||
CurrentTime += elapsed;
|
||||
ElapsedFrameTime = elapsed;
|
||||
FramesPerSecond = Source.FramesPerSecond;
|
||||
}
|
||||
}
|
||||
|
||||
public double ElapsedFrameTime { get; private set; }
|
||||
|
||||
public double FramesPerSecond { get; private set; }
|
||||
|
||||
public FrameTimeInfo TimeInfo => new FrameTimeInfo { Elapsed = ElapsedFrameTime, Current = CurrentTime };
|
||||
|
||||
public Bindable<bool> WaitingOnFrames { get; } = new Bindable<bool>(true);
|
||||
|
||||
public bool IsCatchingUp { get; set; }
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Timing;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
{
|
||||
/// <summary>
|
||||
/// A clock which is used by <see cref="MultiSpectatorPlayer"/>s and managed by an <see cref="ISyncManager"/>.
|
||||
/// </summary>
|
||||
public interface ISpectatorPlayerClock : IFrameBasedClock, IAdjustableClock
|
||||
{
|
||||
/// <summary>
|
||||
/// Starts this <see cref="ISpectatorPlayerClock"/>.
|
||||
/// </summary>
|
||||
new void Start();
|
||||
|
||||
/// <summary>
|
||||
/// Stops this <see cref="ISpectatorPlayerClock"/>.
|
||||
/// </summary>
|
||||
new void Stop();
|
||||
|
||||
/// <summary>
|
||||
/// Whether this clock is waiting on frames to continue playback.
|
||||
/// </summary>
|
||||
Bindable<bool> WaitingOnFrames { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this clock is behind the master clock and running at a higher rate to catch up to it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Of note, this will be false if this clock is *ahead* of the master clock.
|
||||
/// </remarks>
|
||||
bool IsCatchingUp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The source clock
|
||||
/// </summary>
|
||||
IFrameBasedClock Source { set; }
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Timing;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages the synchronisation between one or more <see cref="ISpectatorPlayerClock"/>s in relation to a master clock.
|
||||
/// </summary>
|
||||
public interface ISyncManager
|
||||
{
|
||||
/// <summary>
|
||||
/// An event which is invoked when gameplay is ready to start.
|
||||
/// </summary>
|
||||
event Action ReadyToStart;
|
||||
|
||||
/// <summary>
|
||||
/// The master clock which player clocks should synchronise to.
|
||||
/// </summary>
|
||||
IAdjustableClock MasterClock { get; }
|
||||
|
||||
/// <summary>
|
||||
/// An event which is invoked when the state of <see cref="MasterClock"/> is changed.
|
||||
/// </summary>
|
||||
IBindable<MasterClockState> MasterState { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Adds an <see cref="ISpectatorPlayerClock"/> to manage.
|
||||
/// </summary>
|
||||
/// <param name="clock">The <see cref="ISpectatorPlayerClock"/> to add.</param>
|
||||
void AddPlayerClock(ISpectatorPlayerClock clock);
|
||||
|
||||
/// <summary>
|
||||
/// Removes an <see cref="ISpectatorPlayerClock"/>, stopping it from being managed by this <see cref="ISyncManager"/>.
|
||||
/// </summary>
|
||||
/// <param name="clock">The <see cref="ISpectatorPlayerClock"/> to remove.</param>
|
||||
void RemovePlayerClock(ISpectatorPlayerClock clock);
|
||||
}
|
||||
}
|
@ -2,7 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
@ -14,15 +13,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
/// </summary>
|
||||
public class MultiSpectatorPlayer : SpectatorPlayer
|
||||
{
|
||||
private readonly Bindable<bool> waitingOnFrames = new Bindable<bool>(true);
|
||||
private readonly ISpectatorPlayerClock spectatorPlayerClock;
|
||||
private readonly SpectatorPlayerClock spectatorPlayerClock;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="MultiSpectatorPlayer"/>.
|
||||
/// </summary>
|
||||
/// <param name="score">The score containing the player's replay.</param>
|
||||
/// <param name="spectatorPlayerClock">The clock controlling the gameplay running state.</param>
|
||||
public MultiSpectatorPlayer(Score score, ISpectatorPlayerClock spectatorPlayerClock)
|
||||
public MultiSpectatorPlayer(Score score, SpectatorPlayerClock spectatorPlayerClock)
|
||||
: base(score, new PlayerConfiguration { AllowUserInteraction = false })
|
||||
{
|
||||
this.spectatorPlayerClock = spectatorPlayerClock;
|
||||
@ -31,8 +29,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
spectatorPlayerClock.WaitingOnFrames.BindTo(waitingOnFrames);
|
||||
|
||||
HUDOverlay.PlayerSettingsOverlay.Expire();
|
||||
HUDOverlay.HoldToQuit.Expire();
|
||||
}
|
||||
@ -40,9 +36,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
protected override void Update()
|
||||
{
|
||||
// The player clock's running state is controlled externally, but the local pausing state needs to be updated to start/stop gameplay.
|
||||
CatchUpSpectatorPlayerClock catchUpClock = (CatchUpSpectatorPlayerClock)GameplayClockContainer.SourceClock;
|
||||
|
||||
if (catchUpClock.IsRunning)
|
||||
if (GameplayClockContainer.SourceClock.IsRunning)
|
||||
GameplayClockContainer.Start();
|
||||
else
|
||||
GameplayClockContainer.Stop();
|
||||
@ -55,7 +49,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
base.UpdateAfterChildren();
|
||||
|
||||
// This is required because the frame stable clock is set to WaitingOnFrames = false for one frame.
|
||||
waitingOnFrames.Value = DrawableRuleset.FrameStableClock.WaitingOnFrames.Value || Score.Replay.Frames.Count == 0;
|
||||
spectatorPlayerClock.WaitingOnFrames = DrawableRuleset.FrameStableClock.WaitingOnFrames.Value || Score.Replay.Frames.Count == 0;
|
||||
}
|
||||
|
||||
protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart)
|
||||
|
@ -1,16 +1,12 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
@ -42,18 +38,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
protected override UserActivity InitialActivity => new UserActivity.SpectatingMultiplayerGame(Beatmap.Value.BeatmapInfo, Ruleset.Value);
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; }
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private MultiplayerClient multiplayerClient { get; set; }
|
||||
private MultiplayerClient multiplayerClient { get; set; } = null!;
|
||||
|
||||
private readonly PlayerArea[] instances;
|
||||
private MasterGameplayClockContainer masterClockContainer;
|
||||
private ISyncManager syncManager;
|
||||
private PlayerGrid grid;
|
||||
private MultiSpectatorLeaderboard leaderboard;
|
||||
private PlayerArea currentAudioSource;
|
||||
private bool canStartMasterClock;
|
||||
private MasterGameplayClockContainer masterClockContainer = null!;
|
||||
private SpectatorSyncManager syncManager = null!;
|
||||
private PlayerGrid grid = null!;
|
||||
private MultiSpectatorLeaderboard leaderboard = null!;
|
||||
private PlayerArea? currentAudioSource;
|
||||
|
||||
private readonly Room room;
|
||||
private readonly MultiplayerRoomUser[] users;
|
||||
@ -78,57 +73,58 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
FillFlowContainer leaderboardFlow;
|
||||
Container scoreDisplayContainer;
|
||||
|
||||
masterClockContainer = CreateMasterGameplayClockContainer(Beatmap.Value);
|
||||
|
||||
InternalChildren = new[]
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
(Drawable)(syncManager = new CatchUpSyncManager(masterClockContainer)),
|
||||
masterClockContainer.WithChild(new GridContainer
|
||||
masterClockContainer = new MasterGameplayClockContainer(Beatmap.Value, 0)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
|
||||
Content = new[]
|
||||
Child = new GridContainer
|
||||
{
|
||||
new Drawable[]
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
|
||||
Content = new[]
|
||||
{
|
||||
scoreDisplayContainer = new Container
|
||||
new Drawable[]
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y
|
||||
},
|
||||
},
|
||||
new Drawable[]
|
||||
{
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
|
||||
Content = new[]
|
||||
scoreDisplayContainer = new Container
|
||||
{
|
||||
new Drawable[]
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y
|
||||
},
|
||||
},
|
||||
new Drawable[]
|
||||
{
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
|
||||
Content = new[]
|
||||
{
|
||||
leaderboardFlow = new FillFlowContainer
|
||||
new Drawable[]
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(5)
|
||||
},
|
||||
grid = new PlayerGrid { RelativeSizeAxes = Axes.Both }
|
||||
leaderboardFlow = new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(5)
|
||||
},
|
||||
grid = new PlayerGrid { RelativeSizeAxes = Axes.Both }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
syncManager = new SpectatorSyncManager(masterClockContainer)
|
||||
{
|
||||
ReadyToStart = performInitialSeek,
|
||||
}
|
||||
};
|
||||
|
||||
for (int i = 0; i < Users.Count; i++)
|
||||
{
|
||||
grid.Add(instances[i] = new PlayerArea(Users[i], masterClockContainer));
|
||||
syncManager.AddPlayerClock(instances[i].GameplayClock);
|
||||
}
|
||||
grid.Add(instances[i] = new PlayerArea(Users[i], syncManager.CreateManagedClock()));
|
||||
|
||||
LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(users)
|
||||
{
|
||||
@ -161,9 +157,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
base.LoadComplete();
|
||||
|
||||
masterClockContainer.Reset();
|
||||
|
||||
syncManager.ReadyToStart += onReadyToStart;
|
||||
syncManager.MasterState.BindValueChanged(onMasterStateChanged, true);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
@ -173,7 +166,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
if (!isCandidateAudioSource(currentAudioSource?.GameplayClock))
|
||||
{
|
||||
currentAudioSource = instances.Where(i => isCandidateAudioSource(i.GameplayClock))
|
||||
.OrderBy(i => Math.Abs(i.GameplayClock.CurrentTime - syncManager.MasterClock.CurrentTime))
|
||||
.OrderBy(i => Math.Abs(i.GameplayClock.CurrentTime - syncManager.CurrentMasterTime))
|
||||
.FirstOrDefault();
|
||||
|
||||
foreach (var instance in instances)
|
||||
@ -181,40 +174,21 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
}
|
||||
}
|
||||
|
||||
private bool isCandidateAudioSource([CanBeNull] ISpectatorPlayerClock clock)
|
||||
=> clock?.IsRunning == true && !clock.IsCatchingUp && !clock.WaitingOnFrames.Value;
|
||||
private bool isCandidateAudioSource(SpectatorPlayerClock? clock)
|
||||
=> clock?.IsRunning == true && !clock.IsCatchingUp && !clock.WaitingOnFrames;
|
||||
|
||||
private void onReadyToStart()
|
||||
private void performInitialSeek()
|
||||
{
|
||||
// Seek the master clock to the gameplay time.
|
||||
// This is chosen as the first available frame in the players' replays, which matches the seek by each individual SpectatorPlayer.
|
||||
double startTime = instances.Where(i => i.Score != null)
|
||||
.SelectMany(i => i.Score.Replay.Frames)
|
||||
.SelectMany(i => i.Score.AsNonNull().Replay.Frames)
|
||||
.Select(f => f.Time)
|
||||
.DefaultIfEmpty(0)
|
||||
.Min();
|
||||
|
||||
masterClockContainer.StartTime = startTime;
|
||||
masterClockContainer.Reset(true);
|
||||
|
||||
// Although the clock has been started, this flag is set to allow for later synchronisation state changes to also be able to start it.
|
||||
canStartMasterClock = true;
|
||||
}
|
||||
|
||||
private void onMasterStateChanged(ValueChangedEvent<MasterClockState> state)
|
||||
{
|
||||
switch (state.NewValue)
|
||||
{
|
||||
case MasterClockState.Synchronised:
|
||||
if (canStartMasterClock)
|
||||
masterClockContainer.Start();
|
||||
|
||||
break;
|
||||
|
||||
case MasterClockState.TooFarAhead:
|
||||
masterClockContainer.Stop();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnNewPlayingUserState(int userId, SpectatorState spectatorState)
|
||||
@ -242,7 +216,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
var instance = instances.Single(i => i.UserId == userId);
|
||||
|
||||
instance.FadeColour(colours.Gray4, 400, Easing.OutQuint);
|
||||
syncManager.RemovePlayerClock(instance.GameplayClock);
|
||||
syncManager.RemoveManagedClock(instance.GameplayClock);
|
||||
}
|
||||
|
||||
public override bool OnBackButton()
|
||||
@ -256,7 +230,5 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
|
||||
return base.OnBackButton();
|
||||
}
|
||||
|
||||
protected virtual MasterGameplayClockContainer CreateMasterGameplayClockContainer(WorkingBeatmap beatmap) => new MasterGameplayClockContainer(beatmap, 0);
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +1,13 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets;
|
||||
@ -29,7 +25,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
/// <summary>
|
||||
/// Raised after <see cref="Player.StartGameplay"/> is called on <see cref="Player"/>.
|
||||
/// </summary>
|
||||
public event Action OnGameplayStarted;
|
||||
public event Action? OnGameplayStarted;
|
||||
|
||||
/// <summary>
|
||||
/// Whether a <see cref="Player"/> is loaded in the area.
|
||||
@ -42,25 +38,27 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
public readonly int UserId;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ISpectatorPlayerClock"/> used to control the gameplay running state of a loaded <see cref="Player"/>.
|
||||
/// The <see cref="SpectatorPlayerClock"/> used to control the gameplay running state of a loaded <see cref="Player"/>.
|
||||
/// </summary>
|
||||
[NotNull]
|
||||
public readonly ISpectatorPlayerClock GameplayClock = new CatchUpSpectatorPlayerClock();
|
||||
public readonly SpectatorPlayerClock GameplayClock;
|
||||
|
||||
/// <summary>
|
||||
/// The currently-loaded score.
|
||||
/// </summary>
|
||||
[CanBeNull]
|
||||
public Score Score { get; private set; }
|
||||
public Score? Score { get; private set; }
|
||||
|
||||
[Resolved]
|
||||
private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;
|
||||
|
||||
private readonly BindableDouble volumeAdjustment = new BindableDouble();
|
||||
private readonly Container gameplayContent;
|
||||
private readonly LoadingLayer loadingLayer;
|
||||
private OsuScreenStack stack;
|
||||
private OsuScreenStack? stack;
|
||||
|
||||
public PlayerArea(int userId, IFrameBasedClock masterClock)
|
||||
public PlayerArea(int userId, SpectatorPlayerClock clock)
|
||||
{
|
||||
UserId = userId;
|
||||
GameplayClock = clock;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
Masking = true;
|
||||
@ -77,14 +75,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
};
|
||||
|
||||
audioContainer.AddAdjustment(AdjustableProperty.Volume, volumeAdjustment);
|
||||
|
||||
GameplayClock.Source = masterClock;
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private IBindable<WorkingBeatmap> beatmap { get; set; }
|
||||
|
||||
public void LoadScore([NotNull] Score score)
|
||||
public void LoadScore(Score score)
|
||||
{
|
||||
if (Score != null)
|
||||
throw new InvalidOperationException($"Cannot load a new score on a {nameof(PlayerArea)} that has an existing score.");
|
||||
|
@ -0,0 +1,100 @@
|
||||
// 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.Timing;
|
||||
using osu.Game.Screens.Play;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
{
|
||||
/// <summary>
|
||||
/// A clock which catches up using rate adjustment.
|
||||
/// </summary>
|
||||
public class SpectatorPlayerClock : IFrameBasedClock, IAdjustableClock
|
||||
{
|
||||
/// <summary>
|
||||
/// The catch up rate.
|
||||
/// </summary>
|
||||
private const double catchup_rate = 2;
|
||||
|
||||
private readonly GameplayClockContainer masterClock;
|
||||
|
||||
public double CurrentTime { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this clock is waiting on frames to continue playback.
|
||||
/// </summary>
|
||||
public bool WaitingOnFrames { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this clock is behind the master clock and running at a higher rate to catch up to it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Of note, this will be false if this clock is *ahead* of the master clock.
|
||||
/// </remarks>
|
||||
public bool IsCatchingUp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this spectator clock should be running.
|
||||
/// Use instead of <see cref="Start"/> / <see cref="Stop"/> to control time.
|
||||
/// </summary>
|
||||
public bool IsRunning { get; set; }
|
||||
|
||||
public SpectatorPlayerClock(GameplayClockContainer masterClock)
|
||||
{
|
||||
this.masterClock = masterClock;
|
||||
}
|
||||
|
||||
public void Reset() => CurrentTime = 0;
|
||||
|
||||
public void Start()
|
||||
{
|
||||
// Our running state should only be managed by SpectatorSyncManager via IsRunning.
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
// Our running state should only be managed by an SpectatorSyncManager via IsRunning.
|
||||
}
|
||||
|
||||
public bool Seek(double position)
|
||||
{
|
||||
CurrentTime = position;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void ResetSpeedAdjustments()
|
||||
{
|
||||
}
|
||||
|
||||
public double Rate
|
||||
{
|
||||
get => IsCatchingUp ? catchup_rate : 1;
|
||||
set => throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void ProcessFrame()
|
||||
{
|
||||
if (IsRunning)
|
||||
{
|
||||
double elapsedSource = masterClock.ElapsedFrameTime;
|
||||
double elapsed = elapsedSource * Rate;
|
||||
|
||||
CurrentTime += elapsed;
|
||||
ElapsedFrameTime = elapsed;
|
||||
FramesPerSecond = masterClock.FramesPerSecond;
|
||||
}
|
||||
else
|
||||
{
|
||||
ElapsedFrameTime = 0;
|
||||
FramesPerSecond = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public double ElapsedFrameTime { get; private set; }
|
||||
|
||||
public double FramesPerSecond { get; private set; }
|
||||
|
||||
public FrameTimeInfo TimeInfo => new FrameTimeInfo { Elapsed = ElapsedFrameTime, Current = CurrentTime };
|
||||
}
|
||||
}
|
@ -1,22 +1,19 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Screens.Play;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="ISyncManager"/> which synchronises de-synced player clocks through catchup.
|
||||
/// Manages the synchronisation between one or more <see cref="SpectatorPlayerClock"/>s in relation to a master clock.
|
||||
/// </summary>
|
||||
public class CatchUpSyncManager : Component, ISyncManager
|
||||
public class SpectatorSyncManager : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The offset from the master clock to which player clocks should remain within to be considered in-sync.
|
||||
@ -33,40 +30,53 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
/// </summary>
|
||||
public const double MAXIMUM_START_DELAY = 15000;
|
||||
|
||||
public event Action ReadyToStart;
|
||||
/// <summary>
|
||||
/// An event which is invoked when gameplay is ready to start.
|
||||
/// </summary>
|
||||
public Action? ReadyToStart;
|
||||
|
||||
public double CurrentMasterTime => masterClock.CurrentTime;
|
||||
|
||||
/// <summary>
|
||||
/// The master clock which is used to control the timing of all player clocks clocks.
|
||||
/// </summary>
|
||||
public IAdjustableClock MasterClock { get; }
|
||||
|
||||
public IBindable<MasterClockState> MasterState => masterState;
|
||||
private readonly GameplayClockContainer masterClock;
|
||||
|
||||
/// <summary>
|
||||
/// The player clocks.
|
||||
/// </summary>
|
||||
private readonly List<ISpectatorPlayerClock> playerClocks = new List<ISpectatorPlayerClock>();
|
||||
private readonly List<SpectatorPlayerClock> playerClocks = new List<SpectatorPlayerClock>();
|
||||
|
||||
private readonly Bindable<MasterClockState> masterState = new Bindable<MasterClockState>();
|
||||
private MasterClockState masterState = MasterClockState.Synchronised;
|
||||
|
||||
private bool hasStarted;
|
||||
|
||||
private double? firstStartAttemptTime;
|
||||
|
||||
public CatchUpSyncManager(IAdjustableClock master)
|
||||
public SpectatorSyncManager(GameplayClockContainer master)
|
||||
{
|
||||
MasterClock = master;
|
||||
masterClock = master;
|
||||
}
|
||||
|
||||
public void AddPlayerClock(ISpectatorPlayerClock clock)
|
||||
/// <summary>
|
||||
/// Create a new managed <see cref="SpectatorPlayerClock"/>.
|
||||
/// </summary>
|
||||
/// <returns>The newly created <see cref="SpectatorPlayerClock"/>.</returns>
|
||||
public SpectatorPlayerClock CreateManagedClock()
|
||||
{
|
||||
Debug.Assert(!playerClocks.Contains(clock));
|
||||
var clock = new SpectatorPlayerClock(masterClock);
|
||||
playerClocks.Add(clock);
|
||||
return clock;
|
||||
}
|
||||
|
||||
public void RemovePlayerClock(ISpectatorPlayerClock clock)
|
||||
/// <summary>
|
||||
/// Removes an <see cref="SpectatorPlayerClock"/>, stopping it from being managed by this <see cref="SpectatorSyncManager"/>.
|
||||
/// </summary>
|
||||
/// <param name="clock">The <see cref="SpectatorPlayerClock"/> to remove.</param>
|
||||
public void RemoveManagedClock(SpectatorPlayerClock clock)
|
||||
{
|
||||
playerClocks.Remove(clock);
|
||||
clock.Stop();
|
||||
clock.IsRunning = false;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
@ -77,7 +87,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
{
|
||||
// Ensure all player clocks are stopped until the start succeeds.
|
||||
foreach (var clock in playerClocks)
|
||||
clock.Stop();
|
||||
clock.IsRunning = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@ -97,7 +107,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
if (playerClocks.Count == 0)
|
||||
return false;
|
||||
|
||||
int readyCount = playerClocks.Count(s => !s.WaitingOnFrames.Value);
|
||||
int readyCount = playerClocks.Count(s => !s.WaitingOnFrames);
|
||||
|
||||
if (readyCount == playerClocks.Count)
|
||||
return performStart();
|
||||
@ -130,7 +140,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
|
||||
// How far this player's clock is out of sync, compared to the master clock.
|
||||
// A negative value means the player is running fast (ahead); a positive value means the player is running behind (catching up).
|
||||
double timeDelta = MasterClock.CurrentTime - clock.CurrentTime;
|
||||
double timeDelta = masterClock.CurrentTime - clock.CurrentTime;
|
||||
|
||||
// Check that the player clock isn't too far ahead.
|
||||
// This is a quiet case in which the catchup is done by the master clock, so IsCatchingUp is not set on the player clock.
|
||||
@ -139,15 +149,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
// Importantly, set the clock to a non-catchup state. if this isn't done, updateMasterState may incorrectly pause the master clock
|
||||
// when it is required to be running (ie. if all players are ahead of the master).
|
||||
clock.IsCatchingUp = false;
|
||||
clock.Stop();
|
||||
clock.IsRunning = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Make sure the player clock is running if it can.
|
||||
if (!clock.WaitingOnFrames.Value)
|
||||
clock.Start();
|
||||
else
|
||||
clock.Stop();
|
||||
clock.IsRunning = !clock.WaitingOnFrames;
|
||||
|
||||
if (clock.IsCatchingUp)
|
||||
{
|
||||
@ -169,8 +176,26 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
/// </summary>
|
||||
private void updateMasterState()
|
||||
{
|
||||
bool anyInSync = playerClocks.Any(s => !s.IsCatchingUp);
|
||||
masterState.Value = anyInSync ? MasterClockState.Synchronised : MasterClockState.TooFarAhead;
|
||||
MasterClockState newState = playerClocks.Any(s => !s.IsCatchingUp) ? MasterClockState.Synchronised : MasterClockState.TooFarAhead;
|
||||
|
||||
if (masterState == newState)
|
||||
return;
|
||||
|
||||
masterState = newState;
|
||||
Logger.Log($"{nameof(SpectatorSyncManager)}'s master clock become {masterState}");
|
||||
|
||||
switch (masterState)
|
||||
{
|
||||
case MasterClockState.Synchronised:
|
||||
if (hasStarted)
|
||||
masterClock.Start();
|
||||
|
||||
break;
|
||||
|
||||
case MasterClockState.TooFarAhead:
|
||||
masterClock.Stop();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osuTK;
|
||||
@ -31,7 +32,7 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
protected const float TRANSITION_TIME = 500;
|
||||
|
||||
protected abstract string Message { get; }
|
||||
protected abstract LocalisableString Message { get; }
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
|
||||
|
||||
|
@ -8,7 +8,6 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Beatmaps;
|
||||
@ -84,7 +83,10 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
api.Queue(req);
|
||||
|
||||
tcs.Task.WaitSafely();
|
||||
// Generally a timeout would not happen here as APIAccess will timeout first.
|
||||
if (!tcs.Task.Wait(60000))
|
||||
handleTokenFailure(new InvalidOperationException("Token retrieval timed out (request never run)"));
|
||||
|
||||
return true;
|
||||
|
||||
void handleTokenFailure(Exception exception)
|
||||
|
@ -8,6 +8,7 @@ using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osuTK;
|
||||
@ -59,7 +60,7 @@ namespace osu.Game.Screens.Ranking.Statistics
|
||||
|
||||
private static Drawable createHeader(StatisticItem item)
|
||||
{
|
||||
if (string.IsNullOrEmpty(item.Name))
|
||||
if (LocalisableString.IsNullOrEmpty(item.Name))
|
||||
return Empty();
|
||||
|
||||
return new FillFlowContainer
|
||||
|
@ -7,6 +7,7 @@ using System;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Screens.Ranking.Statistics
|
||||
{
|
||||
@ -18,7 +19,7 @@ namespace osu.Game.Screens.Ranking.Statistics
|
||||
/// <summary>
|
||||
/// The name of this item.
|
||||
/// </summary>
|
||||
public readonly string Name;
|
||||
public readonly LocalisableString Name;
|
||||
|
||||
/// <summary>
|
||||
/// A function returning the <see cref="Drawable"/> content to be displayed.
|
||||
@ -44,11 +45,11 @@ namespace osu.Game.Screens.Ranking.Statistics
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="StatisticItem"/>, to be displayed inside a <see cref="StatisticRow"/> in the results screen.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the item. Can be <see cref="string.Empty"/> to hide the item header.</param>
|
||||
/// <param name="name">The name of the item. Can be <see langword="null"/> to hide the item header.</param>
|
||||
/// <param name="createContent">A function returning the <see cref="Drawable"/> content to be displayed.</param>
|
||||
/// <param name="requiresHitEvents">Whether this item requires hit events. If true, <see cref="CreateContent"/> will not be called if no hit events are available.</param>
|
||||
/// <param name="dimension">The <see cref="Dimension"/> of this item. This can be thought of as the column dimension of an encompassing <see cref="GridContainer"/>.</param>
|
||||
public StatisticItem([NotNull] string name, [NotNull] Func<Drawable> createContent, bool requiresHitEvents = false, [CanBeNull] Dimension dimension = null)
|
||||
public StatisticItem(LocalisableString name, [NotNull] Func<Drawable> createContent, bool requiresHitEvents = false, [CanBeNull] Dimension dimension = null)
|
||||
{
|
||||
Name = name;
|
||||
RequiresHitEvents = requiresHitEvents;
|
||||
|
@ -55,8 +55,6 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
match &= !criteria.Artist.HasFilter || criteria.Artist.Matches(BeatmapInfo.Metadata.Artist) ||
|
||||
criteria.Artist.Matches(BeatmapInfo.Metadata.ArtistUnicode);
|
||||
|
||||
match &= criteria.Sort != SortMode.DateRanked || BeatmapInfo.BeatmapSet?.DateRanked != null;
|
||||
|
||||
match &= !criteria.UserStarDifficulty.HasFilter || criteria.UserStarDifficulty.IsInRange(BeatmapInfo.StarRating);
|
||||
|
||||
if (match && criteria.SearchTerms.Length > 0)
|
||||
|
@ -99,6 +99,13 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
|
||||
case SortMode.Difficulty:
|
||||
return compareUsingAggregateMax(otherSet, b => b.StarRating);
|
||||
|
||||
case SortMode.DateSubmitted:
|
||||
// Beatmaps which have no submitted date should already be filtered away in this mode.
|
||||
if (BeatmapSet.DateSubmitted == null || otherSet.BeatmapSet.DateSubmitted == null)
|
||||
return 0;
|
||||
|
||||
return otherSet.BeatmapSet.DateSubmitted.Value.CompareTo(BeatmapSet.DateSubmitted.Value);
|
||||
}
|
||||
}
|
||||
|
||||
@ -122,7 +129,12 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
public override void Filter(FilterCriteria criteria)
|
||||
{
|
||||
base.Filter(criteria);
|
||||
Filtered.Value = Items.All(i => i.Filtered.Value);
|
||||
bool match = Items.All(i => i.Filtered.Value);
|
||||
|
||||
match &= criteria.Sort != SortMode.DateRanked || BeatmapSet?.DateRanked != null;
|
||||
match &= criteria.Sort != SortMode.DateSubmitted || BeatmapSet?.DateSubmitted != null;
|
||||
|
||||
Filtered.Value = match;
|
||||
}
|
||||
|
||||
public override string ToString() => BeatmapSet.ToString();
|
||||
|
@ -20,6 +20,9 @@ namespace osu.Game.Screens.Select.Filter
|
||||
[LocalisableDescription(typeof(SortStrings), nameof(SortStrings.ArtistTracksBpm))]
|
||||
BPM,
|
||||
|
||||
[Description("Date Submitted")]
|
||||
DateSubmitted,
|
||||
|
||||
[Description("Date Added")]
|
||||
DateAdded,
|
||||
|
||||
|
@ -928,7 +928,7 @@ namespace osu.Game.Screens.Select
|
||||
}
|
||||
}
|
||||
|
||||
private class SoloModSelectOverlay : UserModSelectOverlay
|
||||
internal class SoloModSelectOverlay : UserModSelectOverlay
|
||||
{
|
||||
protected override bool ShowPresets => true;
|
||||
}
|
||||
|
Reference in New Issue
Block a user