Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 42 additions & 16 deletions src/Client/vMenu.Enhanced.Configuration/ClientConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,40 +18,56 @@ public static class ClientConfig

private static readonly ConfigStore Store = new(Native.GetConvar, ForwardLog);

public static event Action? Changed
{
add => Store.Changed += value;
remove => Store.Changed -= value;
}

/// <summary>Call once, before the menus are built, so the first gate pass reads real values.</summary>
public static void Initialize()
{
Store.Prime();

ApplyLogLevel();

// Any convar moving re-reads the level, because Changed does not say which one moved.
Store.Changed += ApplyLogLevel;
Store.Watch([Debugging.LogLevel], ApplyLogLevel);

// One listener per convar rather than a wildcard filter, which if it matched nothing would
// look like the module quietly not working.
foreach (var convar in Store.Tracked)
{
NativeFixer.AddConvarChangeListener(convar, OnConvarChanged);
}
Listen(Store.Tracked);

SharedAPI.Commands.RegisterCommand(DumpCommand, false, DebugCommands.Gate(Dump));
}

/// <summary>Starts watching convars that are not settings, so listeners can be added for them.</summary>
/// <remarks>See <see cref="ConfigStore.Track" /> for why these are not in the catalog.</remarks>
public static void Track(IReadOnlyList<string> convars) => Listen(Store.Track(convars));

/// <summary>Calls <paramref name="handler"/> whenever any of these settings changes, and nothing else.</summary>
public static void AddEventListenerFor(IReadOnlyList<Setting> settings, Action handler) =>
Store.Watch(settings, handler);

/// <summary>The same, for convars registered through <see cref="Track" /> rather than catalogued settings.</summary>
public static void AddEventListenerFor(IReadOnlyList<string> convars, Action handler) =>
Store.Watch(convars, handler);

/// <summary>
/// Calls <paramref name="handler"/> whenever any setting other than these changes. For a
/// subscriber that really does react to almost anything, where naming the settings it reads
/// would mean one added later silently never reaching it.
/// </summary>
public static void AddEventListenerExcept(IReadOnlyList<Setting> settings, Action handler) =>
Store.WatchExcept(settings, handler);

public static void RemoveEventListenerFor(IReadOnlyList<Setting> settings, Action handler) =>
Store.Unwatch(settings, handler);

public static void RemoveEventListenerFor(IReadOnlyList<string> convars, Action handler) =>
Store.Unwatch(convars, handler);

public static void RemoveEventListenerExcept(Action handler) => Store.UnwatchExcept(handler);

/// <summary>Prints what this client currently reads for every setting.</summary>
public static void Dump()
{
Log.Debug("[Config] Current values:");
Log.Info("[Config] Current values:");

foreach (var line in Store.Describe())
{
Log.Debug("[Config] " + line);
Log.Info("[Config] " + line);
}
}

Expand Down Expand Up @@ -79,6 +95,16 @@ public static void Dump()

public static string Value(StringSetting setting) => Store.Value(setting);

// One listener per convar rather than a wildcard filter, which if it matched nothing would look
// like the module quietly not working.
private static void Listen(IReadOnlyList<string> convars)
{
foreach (var convar in convars)
{
NativeFixer.AddConvarChangeListener(convar, OnConvarChanged);
}
}

private static void OnConvarChanged(string convar, object? reserved) => Store.NotifyChanged(convar);

private static void ApplyLogLevel() => Log.SetLevel(Store.Value(Debugging.LogLevel));
Expand Down
1 change: 0 additions & 1 deletion src/Client/vMenu.Enhanced.Core/Main.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ public async void Initialize()

LanguageLoader.Load();

ClientConfig.Changed += TickRegistry.Reevaluate;
ClientPermissions.PermissionsChanged += TickRegistry.Reevaluate;

VehicleCommands.Initialize();
Expand Down
8 changes: 7 additions & 1 deletion src/Client/vMenu.Enhanced.MenuFramework/HeaderStyle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,13 @@ private static readonly (string Name, int Id)[] Fonts =
/// <summary>Call after <see cref="ClientConfig.Initialize"/>, before the menus are built.</summary>
public static void Initialize()
{
ClientConfig.Changed += Apply;
ClientConfig.AddEventListenerFor(
[
AppearanceSettings.TitleAlignment,
AppearanceSettings.TitleFont,
AppearanceSettings.HeaderGlare,
],
Apply);

Apply();
}
Expand Down
28 changes: 26 additions & 2 deletions src/Client/vMenu.Enhanced.MenuFramework/MenuRegistry.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,41 @@
using MenuAPI;

using vMenu.Enhanced.Configuration;
using vMenu.Enhanced.Data.Configuration;
using vMenu.Enhanced.Logging;
using vMenu.Enhanced.MenuFramework.Localization;
using vMenu.Enhanced.Permissions;

using DebuggingSettings = vMenu.Enhanced.Data.Configuration.Settings.Debugging;
using KeyBindingSettings = vMenu.Enhanced.Data.Configuration.Settings.KeyBindings;
using LocalizationSettings = vMenu.Enhanced.Data.Configuration.Settings.Localization;

namespace vMenu.Enhanced.MenuFramework;

/// <summary>Builds the menu tree and keeps it in step with permissions, configuration and language.</summary>
// Subscribes to the three change events once and fans out from here rather than one subscription per
// menu, for one place to unsubscribe and a deterministic order.
public static class MenuRegistry
{
/// <summary>
/// The settings that provably cannot change what any menu shows, so a refresh pass over every
/// gate and every label is not worth running for them.
/// </summary>
private static readonly Setting[] Ignored =
[
DebuggingSettings.LogLevel,

// Read once by LanguageLoader before the menus are built.
// Can't be updated at runtime because the translation files would not be
// streamed to the client if changed without a resource restart.
LocalizationSettings.Languages,

// Keybinds get registered with the game at startup, so changing one takes a restart either way.
KeyBindingSettings.MenuToggleKey,
KeyBindingSettings.NoClipToggleKey,
KeyBindingSettings.TeleportKey,
];

private static readonly List<MenuHost> Hosts = [];

private static readonly Dictionary<Menu, MenuHost> HostsByMenu = new(ReferenceComparer<Menu>.Instance);
Expand Down Expand Up @@ -64,7 +88,7 @@ public static async Task BuildAsync(IReadOnlyList<MenuDefinition> definitions)
await MaterialiseAsync(_root, localizer);

ClientPermissions.PermissionsChanged += RefreshAll;
ClientConfig.Changed += RefreshAll;
ClientConfig.AddEventListenerExcept(Ignored, RefreshAll);
Localizer.Changed += RefreshAll;

// Items are created enabled, so without this everything looks unlocked until the first
Expand Down Expand Up @@ -104,7 +128,7 @@ public static void Dispose()
}

ClientPermissions.PermissionsChanged -= RefreshAll;
ClientConfig.Changed -= RefreshAll;
ClientConfig.RemoveEventListenerExcept(RefreshAll);
Localizer.Changed -= RefreshAll;

foreach (var host in Hosts)
Expand Down
3 changes: 3 additions & 0 deletions src/Client/vMenu.Enhanced.Menus/Developer/DeveloperOverlay.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using CitizenFX.FiveM.Client;

using vMenu.Enhanced.BrokenNatives;
using vMenu.Enhanced.Configuration;
using vMenu.Enhanced.Data.Ticks;
using vMenu.Enhanced.MenuFramework;
using vMenu.Enhanced.Ticks;
Expand Down Expand Up @@ -85,6 +86,8 @@ public static void Initialize()
Condition.Evaluate,
onStopped: Reset);

ClientConfig.AddEventListenerFor([DeveloperFeaturesSetting.Enabled], Reevaluate);

DeveloperFeaturesState.Changed += Reevaluate;
}

Expand Down
42 changes: 22 additions & 20 deletions src/Client/vMenu.Enhanced.Menus/OnlinePlayersMenu.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,13 @@
using MenuAPI;

using vMenu.Enhanced.Actions;
using vMenu.Enhanced.Configuration;
using vMenu.Enhanced.Data.Actions;
using vMenu.Enhanced.Data.OnlinePlayers;
using vMenu.Enhanced.Data.Ticks;
using vMenu.Enhanced.Logging;
using vMenu.Enhanced.MenuFramework;
using vMenu.Enhanced.MenuFramework.Localization;
using vMenu.Enhanced.Menus.Players;
using vMenu.Enhanced.Ticks;

using OnlinePlayersPermissions = vMenu.Enhanced.Data.Permissions.Menus.OnlinePlayers;

Expand Down Expand Up @@ -42,8 +41,6 @@ public sealed class OnlinePlayersMenu : MenuDefinition
/// <summary>Long enough for the longest identifier anybody actually has.</summary>
private const int SearchMaxLength = 64;

private const int StalenessPollMs = 1000;

/// <summary>How much of the search term the subtitle repeats back.</summary>
private const int QueryDisplayLength = 16;

Expand All @@ -57,7 +54,8 @@ public sealed class OnlinePlayersMenu : MenuDefinition

private OnlinePlayer? _selected;

private TickHandle? _staleness;
/// <summary>Whether the list is on screen, since the staleness notice only makes sense there.</summary>
private bool _open;

private string _query = string.Empty;

Expand Down Expand Up @@ -106,7 +104,11 @@ protected override void Build(MenuBuilder menu)
_actions.Builder.OnClosed = _ => _leftForActions = Native.GetGameTimer();

menu.OnOpened = _ => OnOpened();
menu.OnClosed = _ => _staleness?.Stop();
menu.OnClosed = _ => _open = false;

// Not a setting, so the module has to be told about it before anything can listen for it.
ClientConfig.Track([PlayerEvents.RevisionConvar]);
ClientConfig.AddEventListenerFor([PlayerEvents.RevisionConvar], CheckStaleness);
}

private void OnOpened()
Expand All @@ -116,6 +118,8 @@ private void OnOpened()
return;
}

_open = true;

// Rewritten here rather than declared once, so they follow a language change like everything
// else does. MenuAPI's button hints hold a plain string, not a translation key.
var localizer = Localizer.Current;
Expand All @@ -124,16 +128,12 @@ private void OnOpened()
menu.Menu.PreviousPageButtonText = localizer.Get(Loc.OnlinePlayers.PreviousPageButton);
menu.Menu.NextPageButtonText = localizer.Get(Loc.OnlinePlayers.NextPageButton);

_staleness ??= TickRegistry.Register(
"OnlinePlayers.Staleness",
CheckStaleness,
TickRate.Every(StalenessPollMs),
autoStart: false);

_staleness.Start();

if (_leftForActions == Native.GetGameTimer())
{
// Opening the actions menu closed this one, so anything the list missed while it was
// down never reached the listener. This is the one path that keeps the old snapshot.
CheckStaleness();

UpdateSubtitle();

return;
Expand Down Expand Up @@ -184,7 +184,7 @@ private async Task RefreshAsync(string query)
return byName != 0 ? byName : left.ServerId.CompareTo(right.ServerId);
});

_revisionAtBuild = Native.GetConvarInt(PlayerEvents.RevisionConvar, 0);
_revisionAtBuild = Revision();
_outdated = false;
_hasSnapshot = true;

Expand Down Expand Up @@ -600,15 +600,15 @@ private void OnPageChanged(Menu menu, int oldPage, int newPage, bool wrapped)

private void CheckStaleness()
{
// There is nothing to be out of date with until a list has actually arrived. This tick starts
// the moment the menu opens, while the first fetch is still in flight, so without this it
// compares the real revision against a zero nobody ever recorded.
if (!_hasSnapshot || _busy)
// There is nothing to be out of date with until a list has actually arrived, and nothing to
// say about it while the list is not on screen: the notice asks the player to reopen the
// menu, which reads as nonsense to somebody who does not have it open.
if (!_open || !_hasSnapshot || _busy)
{
return;
}

if (_outdated || Native.GetConvarInt(PlayerEvents.RevisionConvar, 0) == _revisionAtBuild)
if (_outdated || Revision() == _revisionAtBuild)
{
return;
}
Expand All @@ -622,6 +622,8 @@ private void CheckStaleness()
Notifications.Warning(MenuText.Key(Loc.OnlinePlayers.OutdatedNotice));
}

private static int Revision() => ClientConfig.GetInt(PlayerEvents.RevisionConvar) ?? 0;

private void UpdateSubtitle()
{
if (_menu is { } menu)
Expand Down
2 changes: 1 addition & 1 deletion src/Client/vMenu.Enhanced.Menus/Players/PvpMode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public static void Initialize()

_initialized = true;

ClientConfig.Changed += Apply;
ClientConfig.AddEventListenerFor([Gameplay.PvpMode], Apply);

Apply();
}
Expand Down
9 changes: 8 additions & 1 deletion src/Client/vMenu.Enhanced.Menus/Vehicles/VehicleCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,14 @@ public static class VehicleCommands
/// <summary>Call after <see cref="ClientConfig.Initialize"/>.</summary>
public static void Initialize()
{
ClientConfig.Changed += Apply;
ClientConfig.AddEventListenerFor(
[
VehicleOptionsSettings.DeleteVehicleCommand,
VehicleOptionsSettings.RepairVehicleCommand,
VehicleOptionsSettings.WashVehicleCommand,
],
Apply);

ClientPermissions.PermissionsChanged += Apply;

Apply();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ private static void Dump()
Log.Info(ClientJson.SerializeIndented(appearance));
}


private static void Labels()
{
if (CurrentVehicle() is not { } handle)
Expand Down
7 changes: 5 additions & 2 deletions src/Client/vMenu.Enhanced.Menus/Vehicles/VehicleSpawning.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public static class VehicleSpawning

var position = ped.Position;
Vector3? velocity = null;
var rpm = 100f;
var rpm = 0f;
var speed = 0f;

if (ped.IsPedInAnyVehicle())
Expand Down Expand Up @@ -100,7 +100,10 @@ public static class VehicleSpawning
newVehicle.Velocity = velocity.Value;
}

Native.SetVehicleCurrentRpm(newVehicle.Handle, rpm);
if (rpm > 0.2f)
{
Native.SetVehicleCurrentRpm(newVehicle.Handle, rpm);
}

ped.SetPedIntoVehicle(newVehicle.Handle, -1);

Expand Down
Loading
Loading