1
0
mirror of https://github.com/ppy/osu.git synced 2026-06-01 06:19:54 +08:00
Files
osu-lazer/osu.Game/OsuGameBase.cs
T
Bartłomiej Dach 0209129618 Extract leaderboard fetch logic from song select beatmap leaderboard drawable
RFC. Another attempt at this.

- Supersedes https://github.com/ppy/osu/pull/31881
- Supersedes / closes https://github.com/ppy/osu/pull/31355
- Closes https://github.com/ppy/osu/issues/29861

This is a weird diff because I am feeling rather boxed in by all the
constraints, namely that:

- Leaderboard state should be global state
- But the global state is essentially managed by song select and namely
  `BeatmapLeaderboard` itself. That's because trying to e.g. not have
  `BeatmapLeaderboard` pass the beatmap and the ruleset to the global
  leaderboard manager is worse, as it essentially introduces two
  parallel paths of execution that need to be somehow merged into one
  (as in I'd have to somehow sync `LeaderboardManager` responding to
  beatmap/ruleset changes with `BeatmapLeaderboard` which is inheritance
  hell)
- Also local leaderboard fetching is data-push (as in the scores can
  change under the leaderboard manager), and online leaderboard fetching
  is data-pull (as in the scores do not change unless the leaderboard
  manager does something). Also online leaderboard fetching can fail.
  Which is why I need to still have the weird setup wherein there's a
  `FetchWithCriteriaAsync()` (because I need to be able to respond to
  online requests taking time, or failing), but also the
  `BeatmapLeaderboard` only uses the public `Scores` bindable to
  actually read the scores (because it needs to respond to new local
  scores arriving).
- Another thing to think about here is what happens when a retrieval
  fails because e.g. the user requested friend leaderboards without
  having supporter. With how this diff is written, that special
  condition is handled to `BeatmapLeaderboard`, and
  `LeaderboardManager`'s state will remain as whatever it was before
  that scope change was requested, which may be considered good or it
  may not (I imagine it's better to show scores in gameplay than not in
  this case, but maybe I'm wrong?)
2025-03-21 12:06:16 +01:00

743 lines
30 KiB
C#

// 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.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Configuration;
using osu.Framework.Development;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input;
using osu.Framework.Input.Handlers;
using osu.Framework.Input.Handlers.Joystick;
using osu.Framework.Input.Handlers.Midi;
using osu.Framework.Input.Handlers.Mouse;
using osu.Framework.Input.Handlers.Tablet;
using osu.Framework.Input.Handlers.Touch;
using osu.Framework.IO.Stores;
using osu.Framework.Localisation;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Timing;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats;
using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.Graphics;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input;
using osu.Game.Input.Bindings;
using osu.Game.IO;
using osu.Game.Localisation;
using osu.Game.Online;
using osu.Game.Online.API;
using osu.Game.Online.Chat;
using osu.Game.Online.Leaderboards;
using osu.Game.Online.Metadata;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Spectator;
using osu.Game.Overlays;
using osu.Game.Overlays.Settings;
using osu.Game.Overlays.Settings.Sections;
using osu.Game.Overlays.Settings.Sections.Input;
using osu.Game.Resources;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Skinning;
using osu.Game.Utils;
using RuntimeInfo = osu.Framework.RuntimeInfo;
namespace osu.Game
{
/// <summary>
/// The most basic <see cref="Game"/> that can be used to host osu! components and systems.
/// Unlike <see cref="OsuGame"/>, this class will not load any kind of UI, allowing it to be used
/// for provide dependencies to test cases without interfering with them.
/// </summary>
[Cached(typeof(OsuGameBase))]
public partial class OsuGameBase : Framework.Game, ICanAcceptFiles, IBeatSyncProvider
{
#if DEBUG
public const string GAME_NAME = "osu! (development)";
#else
public const string GAME_NAME = "osu!";
#endif
public const string OSU_PROTOCOL = "osu://";
public const string CLIENT_STREAM_NAME = @"lazer";
/// <summary>
/// The filename of the main client database.
/// </summary>
public const string CLIENT_DATABASE_FILENAME = @"client.realm";
public const int SAMPLE_CONCURRENCY = 6;
public const double SFX_STEREO_STRENGTH = 0.75;
/// <summary>
/// Length of debounce (in milliseconds) for commonly occuring sample playbacks that could stack.
/// </summary>
public const int SAMPLE_DEBOUNCE_TIME = 20;
/// <summary>
/// The maximum volume at which audio tracks should play back at. This can be set lower than 1 to create some head-room for sound effects.
/// </summary>
private const double global_track_volume_adjust = 0.8;
public virtual bool UseDevelopmentServer => DebugUtils.IsDebugBuild;
public virtual EndpointConfiguration CreateEndpoints() =>
UseDevelopmentServer ? new DevelopmentEndpointConfiguration() : new ProductionEndpointConfiguration();
protected override OnlineStore CreateOnlineStore() => new TrustedDomainOnlineStore();
public virtual Version AssemblyVersion => Assembly.GetEntryAssembly()?.GetName().Version ?? new Version();
/// <summary>
/// MD5 representation of the game executable.
/// </summary>
public string VersionHash { get; private set; }
public bool IsDeployedBuild => AssemblyVersion.Major > 0;
internal const string BUILD_SUFFIX = "lazer";
public virtual string Version
{
get
{
if (!IsDeployedBuild)
return @"local " + (DebugUtils.IsDebugBuild ? @"debug" : @"release");
var version = AssemblyVersion;
return $@"{version.Major}.{version.Minor}.{version.Build}-{BUILD_SUFFIX}";
}
}
/// <summary>
/// The <see cref="Edges"/> that the game should be drawn over at a top level.
/// Defaults to <see cref="Edges.None"/>.
/// </summary>
protected virtual Edges SafeAreaOverrideEdges => Edges.None;
protected OsuConfigManager LocalConfig { get; private set; }
protected SessionStatics SessionStatics { get; private set; }
protected OsuColour Colours { get; private set; }
protected BeatmapManager BeatmapManager { get; private set; }
protected BeatmapModelDownloader BeatmapDownloader { get; private set; }
protected ScoreManager ScoreManager { get; private set; }
protected ScoreModelDownloader ScoreDownloader { get; private set; }
protected SkinManager SkinManager { get; private set; }
protected RealmRulesetStore RulesetStore { get; private set; }
protected RealmKeyBindingStore KeyBindingStore { get; private set; }
protected GlobalCursorDisplay GlobalCursorDisplay { get; private set; }
protected MusicController MusicController { get; private set; }
protected IAPIProvider API { get; set; }
protected Storage Storage { get; set; }
/// <summary>
/// The language in which the game is currently displayed in.
/// </summary>
public Bindable<Language> CurrentLanguage { get; } = new Bindable<Language>();
protected Bindable<WorkingBeatmap> Beatmap { get; private set; } // cached via load() method
/// <summary>
/// The current ruleset selection for the local user.
/// </summary>
[Cached]
[Cached(typeof(IBindable<RulesetInfo>))]
protected internal readonly Bindable<RulesetInfo> Ruleset = new Bindable<RulesetInfo>();
/// <summary>
/// The current mod selection for the local user.
/// </summary>
/// <remarks>
/// If a mod select overlay is present, mod instances set to this value are not guaranteed to remain as the provided instance and will be overwritten by a copy.
/// In such a case, changes to settings of a mod will *not* propagate after a mod is added to this collection.
/// As such, all settings should be finalised before adding a mod to this collection.
/// </remarks>
[Cached]
[Cached(typeof(IBindable<IReadOnlyList<Mod>>))]
protected readonly Bindable<IReadOnlyList<Mod>> SelectedMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
/// <summary>
/// Mods available for the current <see cref="Ruleset"/>.
/// </summary>
public readonly Bindable<Dictionary<ModType, IReadOnlyList<Mod>>> AvailableMods = new Bindable<Dictionary<ModType, IReadOnlyList<Mod>>>(new Dictionary<ModType, IReadOnlyList<Mod>>());
private BeatmapDifficultyCache difficultyCache;
private IBeatmapUpdater beatmapUpdater;
private UserLookupCache userCache;
private BeatmapLookupCache beatmapCache;
private LeaderboardManager leaderboardManager;
private RulesetConfigCache rulesetConfigCache;
private SessionAverageHitErrorTracker hitErrorTracker;
protected SpectatorClient SpectatorClient { get; private set; }
protected MultiplayerClient MultiplayerClient { get; private set; }
private MetadataClient metadataClient;
private RealmAccess realm;
protected SafeAreaContainer SafeAreaContainer { get; private set; }
/// <summary>
/// For now, this is used as a source specifically for beat synced components.
/// Going forward, it could potentially be used as the single source-of-truth for beatmap timing.
/// </summary>
private readonly FramedBeatmapClock beatmapClock = new FramedBeatmapClock(applyOffsets: true, requireDecoupling: false);
protected override Container<Drawable> Content => content;
private Container content;
private DependencyContainer dependencies;
private readonly BindableNumber<double> globalTrackVolumeAdjust = new BindableNumber<double>(global_track_volume_adjust);
private Bindable<string> frameworkLocale = null!;
private IBindable<LocalisationParameters> localisationParameters = null!;
/// <summary>
/// Number of unhandled exceptions to allow before aborting execution.
/// </summary>
/// <remarks>
/// When an unhandled exception is encountered, an internal count will be decremented.
/// If the count hits zero, the game will crash.
/// Each second, the count is incremented until reaching the value specified.
/// </remarks>
protected virtual int UnhandledExceptionsBeforeCrash => DebugUtils.IsDebugBuild ? 0 : 1;
public OsuGameBase()
{
Name = GAME_NAME;
allowableExceptions = UnhandledExceptionsBeforeCrash;
}
[BackgroundDependencyLoader]
private void load(ReadableKeyCombinationProvider keyCombinationProvider, FrameworkConfigManager frameworkConfig)
{
try
{
using (var str = File.OpenRead(typeof(OsuGameBase).Assembly.Location))
VersionHash = str.ComputeMD5Hash();
}
catch
{
// special case for android builds, which can't read DLLs from a packed apk.
// should eventually be handled in a better way.
VersionHash = $"{Version}-{RuntimeInfo.OS}".ComputeMD5Hash();
}
Resources.AddStore(new DllResourceStore(OsuResources.ResourceAssembly));
dependencies.Cache(realm = new RealmAccess(Storage, CLIENT_DATABASE_FILENAME, Host.UpdateThread));
dependencies.CacheAs<RulesetStore>(RulesetStore = new RealmRulesetStore(realm, Storage));
dependencies.CacheAs<IRulesetStore>(RulesetStore);
Decoder.RegisterDependencies(RulesetStore);
dependencies.CacheAs(Storage);
var largeStore = new LargeTextureStore(Host.Renderer, Host.CreateTextureLoaderStore(new NamespacedResourceStore<byte[]>(Resources, @"Textures")));
largeStore.AddTextureSource(Host.CreateTextureLoaderStore(CreateOnlineStore()));
dependencies.Cache(largeStore);
dependencies.CacheAs(LocalConfig);
dependencies.CacheAs<IGameplaySettings>(LocalConfig);
InitialiseFonts();
addFilesWarning();
Audio.Samples.PlaybackConcurrency = SAMPLE_CONCURRENCY;
dependencies.Cache(SkinManager = new SkinManager(Storage, realm, Host, Resources, Audio, Scheduler));
dependencies.CacheAs<ISkinSource>(SkinManager);
EndpointConfiguration endpoints = CreateEndpoints();
MessageFormatter.WebsiteRootUrl = endpoints.WebsiteUrl;
frameworkLocale = frameworkConfig.GetBindable<string>(FrameworkSetting.Locale);
frameworkLocale.BindValueChanged(_ => updateLanguage());
localisationParameters = Localisation.CurrentParameters.GetBoundCopy();
localisationParameters.BindValueChanged(_ => updateLanguage(), true);
CurrentLanguage.BindValueChanged(val => frameworkLocale.Value = val.NewValue.ToCultureCode());
dependencies.CacheAs(API ??= new APIAccess(this, LocalConfig, endpoints, VersionHash));
var defaultBeatmap = new DummyWorkingBeatmap(Audio, Textures);
dependencies.Cache(difficultyCache = new BeatmapDifficultyCache());
// ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup()
dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, realm, API, LocalConfig));
dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, realm, API, Audio, Resources, Host, defaultBeatmap, difficultyCache, performOnlineLookups: true));
dependencies.CacheAs<IWorkingBeatmapCache>(BeatmapManager);
dependencies.Cache(BeatmapDownloader = new BeatmapModelDownloader(BeatmapManager, API));
dependencies.Cache(ScoreDownloader = new ScoreModelDownloader(ScoreManager, API));
// Add after all the above cache operations as it depends on them.
base.Content.Add(difficultyCache);
// TODO: OsuGame or OsuGameBase?
dependencies.CacheAs(beatmapUpdater = CreateBeatmapUpdater());
dependencies.CacheAs(SpectatorClient = new OnlineSpectatorClient(endpoints));
dependencies.CacheAs(MultiplayerClient = new OnlineMultiplayerClient(endpoints));
dependencies.CacheAs(metadataClient = new OnlineMetadataClient(endpoints));
base.Content.Add(new BeatmapOnlineChangeIngest(beatmapUpdater, realm, metadataClient));
BeatmapManager.ProcessBeatmap = (beatmapSet, scope) => beatmapUpdater.Process(beatmapSet, scope);
dependencies.Cache(userCache = new UserLookupCache());
base.Content.Add(userCache);
dependencies.Cache(beatmapCache = new BeatmapLookupCache());
base.Content.Add(beatmapCache);
dependencies.CacheAs<IRulesetConfigCache>(rulesetConfigCache = new RulesetConfigCache(realm, RulesetStore));
var powerStatus = CreateBatteryInfo();
if (powerStatus != null)
dependencies.CacheAs(powerStatus);
dependencies.Cache(SessionStatics = new SessionStatics());
dependencies.Cache(hitErrorTracker = new SessionAverageHitErrorTracker());
dependencies.Cache(Colours = new OsuColour());
RegisterImportHandler(BeatmapManager);
RegisterImportHandler(ScoreManager);
RegisterImportHandler(SkinManager);
// drop track volume game-wide to leave some head-room for UI effects / samples.
// this means that for the time being, gameplay sample playback is louder relative to the audio track, compared to stable.
// we may want to revisit this if users notice or complain about the difference (consider this a bit of a trial).
Audio.Tracks.AddAdjustment(AdjustableProperty.Volume, globalTrackVolumeAdjust);
Beatmap = new NonNullableBindable<WorkingBeatmap>(defaultBeatmap);
dependencies.CacheAs<IBindable<WorkingBeatmap>>(Beatmap);
dependencies.CacheAs(Beatmap);
dependencies.Cache(leaderboardManager = new LeaderboardManager());
base.Content.Add(leaderboardManager);
// add api components to hierarchy.
if (API is APIAccess apiAccess)
base.Content.Add(apiAccess);
base.Content.Add(SpectatorClient);
base.Content.Add(MultiplayerClient);
base.Content.Add(metadataClient);
base.Content.Add(rulesetConfigCache);
PreviewTrackManager previewTrackManager;
dependencies.Cache(previewTrackManager = new PreviewTrackManager(BeatmapManager.BeatmapTrackStore));
base.Content.Add(previewTrackManager);
base.Content.Add(MusicController = new MusicController());
dependencies.CacheAs(MusicController);
MusicController.TrackChanged += onTrackChanged;
base.Content.Add(beatmapClock);
GlobalActionContainer globalBindings;
OsuMenuSamples menuSamples;
dependencies.Cache(menuSamples = new OsuMenuSamples());
base.Content.Add(menuSamples);
base.Content.Add(SafeAreaContainer = new SafeAreaContainer
{
SafeAreaOverrideEdges = SafeAreaOverrideEdges,
RelativeSizeAxes = Axes.Both,
Child = CreateScalingContainer().WithChild(globalBindings = new GlobalActionContainer(this)
{
Children = new Drawable[]
{
(GlobalCursorDisplay = new GlobalCursorDisplay
{
RelativeSizeAxes = Axes.Both
}).WithChild(content = new OsuTooltipContainer(GlobalCursorDisplay.MenuCursor)
{
RelativeSizeAxes = Axes.Both
}),
}
})
});
base.Content.Add(new TouchInputInterceptor());
base.Content.Add(hitErrorTracker);
KeyBindingStore = new RealmKeyBindingStore(realm, keyCombinationProvider);
KeyBindingStore.Register(globalBindings, RulesetStore.AvailableRulesets);
dependencies.Cache(KeyBindingStore);
dependencies.Cache(globalBindings);
Ruleset.BindValueChanged(onRulesetChanged);
Beatmap.BindValueChanged(onBeatmapChanged);
}
private void updateLanguage() => CurrentLanguage.Value = LanguageExtensions.GetLanguageFor(frameworkLocale.Value, localisationParameters.Value);
private void addFilesWarning()
{
var realmStore = new RealmFileStore(realm, Storage);
const string filename = "IMPORTANT READ ME.txt";
if (!realmStore.Storage.Exists(filename))
{
using (var stream = realmStore.Storage.CreateFileSafely(filename))
using (var textWriter = new StreamWriter(stream))
{
textWriter.WriteLine(@"This folder contains all your user files (beatmaps, skins, replays etc.)");
textWriter.WriteLine(@"Please do not touch or delete this folder!!");
textWriter.WriteLine();
textWriter.WriteLine(@"If you are really looking to completely delete user data, please delete");
textWriter.WriteLine(@"the parent folder including all other files and directories");
textWriter.WriteLine();
textWriter.WriteLine(@"For more information on how these files are organised,");
textWriter.WriteLine(@"see https://github.com/ppy/osu/wiki/User-file-storage");
}
}
}
private void onTrackChanged(WorkingBeatmap beatmap, TrackChangeDirection direction) => beatmapClock.ChangeSource(beatmap.Track);
protected virtual void InitialiseFonts()
{
AddFont(Resources, @"Fonts/osuFont");
AddFont(Resources, @"Fonts/Torus/Torus-Regular");
AddFont(Resources, @"Fonts/Torus/Torus-Light");
AddFont(Resources, @"Fonts/Torus/Torus-SemiBold");
AddFont(Resources, @"Fonts/Torus/Torus-Bold");
AddFont(Resources, @"Fonts/Torus-Alternate/Torus-Alternate-Regular");
AddFont(Resources, @"Fonts/Torus-Alternate/Torus-Alternate-Light");
AddFont(Resources, @"Fonts/Torus-Alternate/Torus-Alternate-SemiBold");
AddFont(Resources, @"Fonts/Torus-Alternate/Torus-Alternate-Bold");
AddFont(Resources, @"Fonts/Inter/Inter-Regular");
AddFont(Resources, @"Fonts/Inter/Inter-RegularItalic");
AddFont(Resources, @"Fonts/Inter/Inter-Light");
AddFont(Resources, @"Fonts/Inter/Inter-LightItalic");
AddFont(Resources, @"Fonts/Inter/Inter-SemiBold");
AddFont(Resources, @"Fonts/Inter/Inter-SemiBoldItalic");
AddFont(Resources, @"Fonts/Inter/Inter-Bold");
AddFont(Resources, @"Fonts/Inter/Inter-BoldItalic");
AddFont(Resources, @"Fonts/Noto/Noto-Basic");
AddFont(Resources, @"Fonts/Noto/Noto-Hangul");
AddFont(Resources, @"Fonts/Noto/Noto-CJK-Basic");
AddFont(Resources, @"Fonts/Noto/Noto-CJK-Compatibility");
AddFont(Resources, @"Fonts/Noto/Noto-Thai");
AddFont(Resources, @"Fonts/Venera/Venera-Light");
AddFont(Resources, @"Fonts/Venera/Venera-Bold");
AddFont(Resources, @"Fonts/Venera/Venera-Black");
Fonts.AddStore(new OsuIcon.OsuIconStore(Textures));
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) =>
dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
public override void SetHost(GameHost host)
{
base.SetHost(host);
// may be non-null for certain tests
Storage ??= host.Storage;
LocalConfig ??= UseDevelopmentServer
? new DevelopmentOsuConfigManager(Storage)
: new OsuConfigManager(Storage);
host.ExceptionThrown += onExceptionThrown;
}
/// <summary>
/// Use to programatically exit the game as if the user was triggering via alt-f4.
/// By default, will keep persisting until an exit occurs (exit may be blocked multiple times).
/// May be interrupted (see <see cref="OsuGame"/>'s override).
/// </summary>
public virtual void AttemptExit()
{
if (!OnExiting())
Exit();
else
Scheduler.AddDelayed(AttemptExit, 2000);
}
/// <summary>
/// If supported by the platform, the game will automatically restart after the next exit.
/// </summary>
/// <returns>Whether a restart operation was queued.</returns>
public virtual bool RestartAppWhenExited() => false;
/// <summary>
/// Perform migration of user data to a specified path.
/// </summary>
/// <param name="path">The path to migrate to.</param>
/// <returns>Whether migration succeeded to completion. If <c>false</c>, some files were left behind.</returns>
/// <exception cref="TimeoutException"></exception>
public bool Migrate(string path)
{
Logger.Log($@"Migrating osu! data from ""{Storage.GetFullPath(string.Empty)}"" to ""{path}""...");
IDisposable realmBlocker = null;
try
{
ManualResetEventSlim readyToRun = new ManualResetEventSlim();
bool success = false;
Scheduler.Add(() =>
{
try
{
realmBlocker = realm.BlockAllOperations("migration");
success = true;
}
catch (Exception ex)
{
Logger.Log($"Attempting to block all operations failed: {ex}", LoggingTarget.Database);
}
readyToRun.Set();
}, false);
if (!readyToRun.Wait(30000) || !success)
throw new TimeoutException("Attempting to block for migration took too long.");
bool? cleanupSucceeded = (Storage as OsuStorage)?.Migrate(Host.GetStorage(path));
Logger.Log(@"Migration complete!");
return cleanupSucceeded != false;
}
finally
{
realmBlocker?.Dispose();
}
}
protected virtual IBeatmapUpdater CreateBeatmapUpdater() => new BeatmapUpdater(BeatmapManager, difficultyCache, API, Storage);
protected override UserInputManager CreateUserInputManager() => new OsuUserInputManager();
protected virtual BatteryInfo CreateBatteryInfo() => null;
protected virtual Container CreateScalingContainer() => new DrawSizePreservingFillContainer();
protected override Storage CreateStorage(GameHost host, Storage defaultStorage) => new OsuStorage(host, defaultStorage);
/// <summary>
/// Creates an input settings subsection for an <see cref="InputHandler"/>.
/// </summary>
/// <remarks>Should be overriden per-platform to provide settings for platform-specific handlers.</remarks>
public virtual SettingsSubsection CreateSettingsSubsectionFor(InputHandler handler)
{
// One would think that this could be moved to the `OsuGameDesktop` class, but doing so means that
// OsuGameTestScenes will not show any input options (as they are based on OsuGame not OsuGameDesktop).
//
// This in turn makes it hard for ruleset creators to adjust input settings while testing their ruleset
// within the test browser interface.
if (RuntimeInfo.IsDesktop)
{
switch (handler)
{
case ITabletHandler th:
return new TabletSettings(th);
}
}
switch (handler)
{
case MouseHandler mh:
return new MouseSettings(mh);
case JoystickHandler jh:
return new JoystickSettings(jh);
case TouchHandler th:
return new TouchSettings(th);
case MidiHandler:
return new InputSection.HandlerSection(handler);
// return null for handlers that shouldn't have settings.
default:
return null;
}
}
private void onBeatmapChanged(ValueChangedEvent<WorkingBeatmap> beatmap)
{
if (IsLoaded && !ThreadSafety.IsUpdateThread)
throw new InvalidOperationException("Global beatmap bindable must be changed from update thread.");
Logger.Log($"Game-wide working beatmap updated to {beatmap.NewValue}");
}
private void onRulesetChanged(ValueChangedEvent<RulesetInfo> r)
{
if (IsLoaded && !ThreadSafety.IsUpdateThread)
throw new InvalidOperationException("Global ruleset bindable must be changed from update thread.");
Ruleset instance = null;
try
{
if (r.NewValue?.Available == true)
{
instance = r.NewValue.CreateInstance();
}
}
catch (Exception e)
{
Logger.Error(e, "Ruleset load failed and has been rolled back");
}
if (instance == null)
{
// reject the change if the ruleset is not available.
revertRulesetChange();
return;
}
var dict = new Dictionary<ModType, IReadOnlyList<Mod>>();
try
{
foreach (ModType type in Enum.GetValues<ModType>())
{
dict[type] = instance.GetModsFor(type)
// Rulesets should never return null mods, but let's be defensive just in case.
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
.Where(mod => mod != null)
.ToList();
}
}
catch (Exception e)
{
Logger.Error(e, $"Could not load mods for \"{instance.RulesetInfo.Name}\" ruleset. Current ruleset has been rolled back.");
revertRulesetChange();
return;
}
AvailableMods.Value = dict;
if (SelectedMods.Disabled)
return;
var convertedMods = SelectedMods.Value.Select(mod =>
{
var newMod = instance.CreateModFromAcronym(mod.Acronym);
newMod?.CopyCommonSettingsFrom(mod);
return newMod;
}).Where(newMod => newMod != null).ToList();
if (!ModUtils.CheckValidForGameplay(convertedMods, out var invalid))
invalid.ForEach(newMod => convertedMods.Remove(newMod));
SelectedMods.Value = convertedMods;
void revertRulesetChange() => Ruleset.Value = r.OldValue?.Available == true ? r.OldValue : RulesetStore.AvailableRulesets.First();
}
private int allowableExceptions;
/// <summary>
/// Allows a maximum of one unhandled exception, per second of execution.
/// </summary>
/// <returns>Whether to ignore the exception and continue running.</returns>
private bool onExceptionThrown(Exception ex)
{
if (Interlocked.Decrement(ref allowableExceptions) < 0)
{
Logger.Log("Too many unhandled exceptions, crashing out.");
RulesetStore?.TryDisableCustomRulesetsCausing(ex);
return false;
}
Logger.Log($"Unhandled exception has been allowed with {allowableExceptions} more allowable exceptions.");
// restore the stock of allowable exceptions after a short delay.
Task.Delay(1000).ContinueWith(_ => Interlocked.Increment(ref allowableExceptions));
return true;
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
RulesetStore?.Dispose();
LocalConfig?.Dispose();
beatmapUpdater?.Dispose();
realm?.Dispose();
if (Host != null)
Host.ExceptionThrown -= onExceptionThrown;
}
ControlPointInfo IBeatSyncProvider.ControlPoints => Beatmap.Value.BeatmapLoaded ? Beatmap.Value.Beatmap.ControlPointInfo : null;
IClock IBeatSyncProvider.Clock => beatmapClock;
ChannelAmplitudes IHasAmplitudes.CurrentAmplitudes => Beatmap.Value.TrackLoaded ? Beatmap.Value.Track.CurrentAmplitudes : ChannelAmplitudes.Empty;
}
}