From b02d49278913950ab28eafae1acc34fd3858d91e Mon Sep 17 00:00:00 2001
From: Ricky Merc <65817116+RickyB505@users.noreply.github.com>
Date: Sat, 15 Aug 2026 05:52:19 -0400
Subject: [PATCH 1/3] fix: optimization and fix vehicle spawning so its not
scuffed for some
---
.../Vehicles/VehicleSpawning.cs | 7 ++++--
.../vMenu.Enhanced.Menus/World/WorldState.cs | 24 ++++++++++++++++---
.../vMenu.Enhanced.Menus/World/WorldTime.cs | 2 +-
.../World/WorldWeather.cs | 4 ++--
4 files changed, 29 insertions(+), 8 deletions(-)
diff --git a/src/Client/vMenu.Enhanced.Menus/Vehicles/VehicleSpawning.cs b/src/Client/vMenu.Enhanced.Menus/Vehicles/VehicleSpawning.cs
index 308556bf..2bb53a2a 100644
--- a/src/Client/vMenu.Enhanced.Menus/Vehicles/VehicleSpawning.cs
+++ b/src/Client/vMenu.Enhanced.Menus/Vehicles/VehicleSpawning.cs
@@ -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())
@@ -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);
diff --git a/src/Client/vMenu.Enhanced.Menus/World/WorldState.cs b/src/Client/vMenu.Enhanced.Menus/World/WorldState.cs
index 0c2e03c4..5dad46c7 100644
--- a/src/Client/vMenu.Enhanced.Menus/World/WorldState.cs
+++ b/src/Client/vMenu.Enhanced.Menus/World/WorldState.cs
@@ -31,7 +31,10 @@ public static class WorldState
private const double HardResyncSeconds = 30.0;
private const double DriftCorrection = 0.1;
-
+ private static float _speedMultiplier = ClientConfig.Value(TimeOptionsSettings.SpeedMultiplier);
+ public static int TimeTransitionSeconds = ClientConfig.Value(TimeOptionsSettings.TransitionSeconds);
+ public static int WeatherTransitionSeconds = ClientConfig.Value(WeatherOptionsSettings.TransitionSeconds);
+ public static bool SyncClouds = ClientConfig.Value(WeatherOptionsSettings.SyncClouds);
private static double _anchorUnix;
private static int _anchorTimerMs;
private static bool _anchored;
@@ -57,7 +60,7 @@ public static class WorldState
// Read live rather than cached, so raising it takes effect without a restart. The same convar
// reaches every client, so nobody's sky runs at a different speed from anybody else's.
public static double TimeSpeed =>
- GameClock.ClampSpeed(ClientConfig.Value(TimeOptionsSettings.SpeedMultiplier));
+ GameClock.ClampSpeed(_speedMultiplier);
/// The clock with the server's offset applied, as an in-game second of day.
public static double SecondOfDay =>
@@ -85,7 +88,22 @@ public static void Initialize()
TickRate.Every(PollIntervalMs),
IsNeeded,
onStarted: Poll);
-
+ BrokenNatives.NativeFixer.AddConvarChangeListener(TimeOptionsSettings.SpeedMultiplier.Name, (string convar, object? newValue) =>
+ {
+ _speedMultiplier = ClientConfig.GetFloat(convar) ?? _speedMultiplier;
+ });
+ BrokenNatives.NativeFixer.AddConvarChangeListener(TimeOptionsSettings.TransitionSeconds.Name, (string convar, object? newValue) =>
+ {
+ TimeTransitionSeconds = ClientConfig.GetInt(convar) ?? TimeTransitionSeconds;
+ });
+ BrokenNatives.NativeFixer.AddConvarChangeListener(WeatherOptionsSettings.TransitionSeconds.Name, (string convar, object? newValue) =>
+ {
+ WeatherTransitionSeconds = ClientConfig.GetInt(convar) ?? WeatherTransitionSeconds;
+ });
+ BrokenNatives.NativeFixer.AddConvarChangeListener(WeatherOptionsSettings.SyncClouds.Name, (string convar, object? newValue) =>
+ {
+ SyncClouds = ClientConfig.GetBool(convar) ?? SyncClouds;
+ });
SharedAPI.Commands.RegisterCommand(DumpCommand, false, DebugCommands.Gate(Dump));
}
diff --git a/src/Client/vMenu.Enhanced.Menus/World/WorldTime.cs b/src/Client/vMenu.Enhanced.Menus/World/WorldTime.cs
index 1854b5e3..93309fbe 100644
--- a/src/Client/vMenu.Enhanced.Menus/World/WorldTime.cs
+++ b/src/Client/vMenu.Enhanced.Menus/World/WorldTime.cs
@@ -157,7 +157,7 @@ private static double Ramp()
_ramping = true;
}
- var seconds = Math.Max(0, ClientConfig.Value(TimeOptionsSettings.TransitionSeconds));
+ var seconds = Math.Max(0, WorldState.TimeTransitionSeconds);
if (seconds <= 0)
{
diff --git a/src/Client/vMenu.Enhanced.Menus/World/WorldWeather.cs b/src/Client/vMenu.Enhanced.Menus/World/WorldWeather.cs
index 25bfc31d..87673793 100644
--- a/src/Client/vMenu.Enhanced.Menus/World/WorldWeather.cs
+++ b/src/Client/vMenu.Enhanced.Menus/World/WorldWeather.cs
@@ -123,7 +123,7 @@ private static void Apply()
_wasForced = forced is not null;
// A joining player gets the sky it should already be under, so no fade on the first pass.
- if (ClientConfig.Value(WeatherOptionsSettings.SyncClouds))
+ if (WorldState.SyncClouds)
{
WorldClouds.Apply(CloudTarget(forced, schedule), first ? 0.0f : TransitionSeconds());
}
@@ -163,7 +163,7 @@ private static void Set(WeatherType from, WeatherType to, double percent) =>
Native.SetWeatherTypeTransition(Hashes[(int)from], Hashes[(int)to], (float)percent);
private static float TransitionSeconds() =>
- Math.Max(0, ClientConfig.Value(WeatherOptionsSettings.TransitionSeconds));
+ Math.Max(0, WorldState.WeatherTransitionSeconds);
// Swaps at the moment the sky starts moving rather than when the schedule flips, so the clouds
// and the weather arrive together instead of the clouds lagging a boundary window behind.
From 2d3a4a5fdeeca3ec09da61d12cac869a8f4d3369 Mon Sep 17 00:00:00 2001
From: Tom Grobbe
Date: Sat, 15 Aug 2026 14:10:12 +0200
Subject: [PATCH 2/3] feat(config): listen for specific convars instead of
every change
---
.../ClientConfig.cs | 58 ++++--
src/Client/vMenu.Enhanced.Core/Main.cs | 1 -
.../HeaderStyle.cs | 8 +-
.../MenuRegistry.cs | 28 ++-
.../Developer/DeveloperOverlay.cs | 3 +
.../vMenu.Enhanced.Menus/Players/PvpMode.cs | 2 +-
.../Vehicles/VehicleCommands.cs | 9 +-
.../Vehicles/VehicleDumpCommands.cs | 2 +-
.../vMenu.Enhanced.Menus/World/WorldState.cs | 150 ++++++++--------
.../vMenu.Enhanced.Menus/World/WorldTime.cs | 4 +-
.../World/WorldWeather.cs | 4 +-
.../ServerClock.cs | 8 +-
.../ServerConfig.cs | 26 ++-
.../vMenu.Enhanced.Core.Server/CoreServer.cs | 2 -
.../Configuration/ConfigStore.cs | 165 +++++++++++++++++-
.../World/WorldStateConvars.cs | 3 +
16 files changed, 352 insertions(+), 121 deletions(-)
diff --git a/src/Client/vMenu.Enhanced.Configuration/ClientConfig.cs b/src/Client/vMenu.Enhanced.Configuration/ClientConfig.cs
index 1620faa5..4602d4a9 100644
--- a/src/Client/vMenu.Enhanced.Configuration/ClientConfig.cs
+++ b/src/Client/vMenu.Enhanced.Configuration/ClientConfig.cs
@@ -18,12 +18,6 @@ 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;
- }
-
/// Call once, before the menus are built, so the first gate pass reads real values.
public static void Initialize()
{
@@ -31,27 +25,49 @@ public static void Initialize()
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));
}
+ /// Starts watching convars that are not settings, so listeners can be added for them.
+ /// See for why these are not in the catalog.
+ public static void Track(IReadOnlyList convars) => Listen(Store.Track(convars));
+
+ /// Calls whenever any of these settings changes, and nothing else.
+ public static void AddEventListenerFor(IReadOnlyList settings, Action handler) =>
+ Store.Watch(settings, handler);
+
+ /// The same, for convars registered through rather than catalogued settings.
+ public static void AddEventListenerFor(IReadOnlyList convars, Action handler) =>
+ Store.Watch(convars, handler);
+
+ ///
+ /// Calls 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.
+ ///
+ public static void AddEventListenerExcept(IReadOnlyList settings, Action handler) =>
+ Store.WatchExcept(settings, handler);
+
+ public static void RemoveEventListenerFor(IReadOnlyList settings, Action handler) =>
+ Store.Unwatch(settings, handler);
+
+ public static void RemoveEventListenerFor(IReadOnlyList convars, Action handler) =>
+ Store.Unwatch(convars, handler);
+
+ public static void RemoveEventListenerExcept(Action handler) => Store.UnwatchExcept(handler);
+
/// Prints what this client currently reads for every setting.
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);
}
}
@@ -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 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));
diff --git a/src/Client/vMenu.Enhanced.Core/Main.cs b/src/Client/vMenu.Enhanced.Core/Main.cs
index 40a0df95..652d8cff 100644
--- a/src/Client/vMenu.Enhanced.Core/Main.cs
+++ b/src/Client/vMenu.Enhanced.Core/Main.cs
@@ -82,7 +82,6 @@ public async void Initialize()
LanguageLoader.Load();
- ClientConfig.Changed += TickRegistry.Reevaluate;
ClientPermissions.PermissionsChanged += TickRegistry.Reevaluate;
VehicleCommands.Initialize();
diff --git a/src/Client/vMenu.Enhanced.MenuFramework/HeaderStyle.cs b/src/Client/vMenu.Enhanced.MenuFramework/HeaderStyle.cs
index 95b59c5b..aad4a575 100644
--- a/src/Client/vMenu.Enhanced.MenuFramework/HeaderStyle.cs
+++ b/src/Client/vMenu.Enhanced.MenuFramework/HeaderStyle.cs
@@ -26,7 +26,13 @@ private static readonly (string Name, int Id)[] Fonts =
/// Call after , before the menus are built.
public static void Initialize()
{
- ClientConfig.Changed += Apply;
+ ClientConfig.AddEventListenerFor(
+ [
+ AppearanceSettings.TitleAlignment,
+ AppearanceSettings.TitleFont,
+ AppearanceSettings.HeaderGlare,
+ ],
+ Apply);
Apply();
}
diff --git a/src/Client/vMenu.Enhanced.MenuFramework/MenuRegistry.cs b/src/Client/vMenu.Enhanced.MenuFramework/MenuRegistry.cs
index 95f95ee5..3c6e00b1 100644
--- a/src/Client/vMenu.Enhanced.MenuFramework/MenuRegistry.cs
+++ b/src/Client/vMenu.Enhanced.MenuFramework/MenuRegistry.cs
@@ -1,10 +1,15 @@
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;
/// Builds the menu tree and keeps it in step with permissions, configuration and language.
@@ -12,6 +17,25 @@ namespace vMenu.Enhanced.MenuFramework;
// menu, for one place to unsubscribe and a deterministic order.
public static class MenuRegistry
{
+ ///
+ /// 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.
+ ///
+ 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 Hosts = [];
private static readonly Dictionary