using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Reflection;
using System.Resources;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using System.Text;
using BetterJump;
using BetterJump.Config;
using BetterJump.Runtime;
using DooDesch;
using HarmonyLib;
using MelonLoader;
using MelonLoader.Preferences;
using Microsoft.CodeAnalysis;
using Mimic.Actors;
using MimicAPI.GameAPI;
using Steamworks;
using UnityEngine;
[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: MelonInfo(typeof(Core), "BetterJump", "1.6.0", "DooDesch", null)]
[assembly: MelonGame("ReLUGames", "MIMESIS")]
[assembly: MelonOptionalDependencies(new string[] { "MimicAPI" })]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("DooDesch")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyCopyright("Copyright © DooDesch")]
[assembly: AssemblyFileVersion("1.6.0.0")]
[assembly: AssemblyInformationalVersion("1.6.0+8c706b4dae45671d76f6b7bbc3b5a2e1ece78d6e")]
[assembly: AssemblyProduct("BetterJump")]
[assembly: AssemblyTitle("BetterJump")]
[assembly: NeutralResourcesLanguage("en-US")]
[assembly: AssemblyMetadata("AI_Assisted_Creation", "This assembly was partially created with the assistance of Generative AI.")]
[assembly: AssemblyMetadata("AI_Model_Vendor", "Anthropic/OpenAI")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("1.6.0.0")]
[module: UnverifiableCode]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
[CompilerGenerated]
[Embedded]
internal sealed class EmbeddedAttribute : Attribute
{
}
}
namespace System.Runtime.CompilerServices
{
[CompilerGenerated]
[Embedded]
[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
internal sealed class RefSafetyRulesAttribute : Attribute
{
public readonly int Version;
public RefSafetyRulesAttribute(int P_0)
{
Version = P_0;
}
}
}
namespace DooDesch
{
internal sealed class HostSettings
{
[HarmonyPatch(typeof(SteamInviteDispatcher), "OnLobbyCreated")]
private static class CreatedPatch
{
private static void Postfix(LobbyCreated_t callback)
{
//IL_0000: Unknown result type (might be due to invalid IL or missing references)
//IL_0001: Unknown result type (might be due to invalid IL or missing references)
//IL_0007: Invalid comparison between Unknown and I4
//IL_001e: Unknown result type (might be due to invalid IL or missing references)
//IL_0024: Unknown result type (might be due to invalid IL or missing references)
if ((int)callback.m_eResult != 1)
{
return;
}
foreach (HostSettings instance in Instances)
{
instance.Begin(new CSteamID(callback.m_ulSteamIDLobby), host: true);
}
}
}
[HarmonyPatch(typeof(SteamInviteDispatcher), "OnLobbyEntered")]
private static class EnteredPatch
{
private static void Prefix(LobbyEnter_t callback)
{
//IL_0000: Unknown result type (might be due to invalid IL or missing references)
//IL_000c: Unknown result type (might be due to invalid IL or missing references)
//IL_0043: Unknown result type (might be due to invalid IL or missing references)
//IL_0035: Unknown result type (might be due to invalid IL or missing references)
//IL_003a: Unknown result type (might be due to invalid IL or missing references)
if (callback.m_EChatRoomEnterResponse != 1)
{
return;
}
CSteamID val = default(CSteamID);
((CSteamID)(ref val))..ctor(callback.m_ulSteamIDLobby);
foreach (HostSettings instance in Instances)
{
if (!instance.isHost || !(instance.lobby == val))
{
instance.Begin(val, host: false);
}
}
}
}
[HarmonyPatch(typeof(SteamMatchmaking), "LeaveLobby")]
private static class LeavePatch
{
private static void Prefix(CSteamID steamIDLobby)
{
//IL_0016: Unknown result type (might be due to invalid IL or missing references)
//IL_001b: Unknown result type (might be due to invalid IL or missing references)
foreach (HostSettings instance in Instances)
{
if (instance.lobby == steamIDLobby)
{
instance.Reset();
}
}
}
}
[HarmonyPatch(typeof(MainMenu), "Start")]
private static class MenuPatch
{
private static void Prefix()
{
foreach (HostSettings instance in Instances)
{
instance.Reset();
}
}
}
private static readonly List<HostSettings> Instances = new List<HostSettings>();
private static Callback<LobbyDataUpdate_t> dataCallback;
private static float nextPoll;
private readonly string modId;
private readonly string lobbyKey;
private readonly int schemaVersion;
private readonly List<ISetting> settings = new List<ISetting>();
private readonly string modernKey;
private string modernPayload;
private string lastModernPayload;
private long modernRevision;
private bool legacyHost;
private Dictionary<string, object> snapshot;
private CSteamID lobby;
private CSteamID owner;
private bool isHost;
private bool hostCreated;
private bool authorityValid;
private bool ownerChanged;
private bool published;
private string hostPayload;
private string warningReason;
private int warningRepeats;
internal bool IsInSession => lobby != CSteamID.Nil;
internal bool IsHost
{
get
{
if (isHost)
{
return authorityValid;
}
return false;
}
}
internal bool UsingLegacyHost
{
get
{
if (!isHost && authorityValid && legacyHost)
{
return modernRevision == 0;
}
return false;
}
}
internal event Action EffectiveValuesChanged;
internal event Action LiveValuesChanged;
internal HostSettings(string modId, int schemaVersion = 1)
{
if (string.IsNullOrWhiteSpace(modId))
{
throw new ArgumentException("A mod ID is required.", "modId");
}
if (schemaVersion < 1)
{
throw new ArgumentOutOfRangeException("schemaVersion");
}
this.modId = modId;
this.schemaVersion = schemaVersion;
lobbyKey = "doodesch." + modId.ToLowerInvariant() + ".settings.v" + schemaVersion;
modernKey = lobbyKey + ".extended";
if (Encoding.UTF8.GetByteCount(modernKey) >= 255)
{
throw new ArgumentException("The mod ID exceeds the Steam lobby key limit.", "modId");
}
foreach (HostSettings instance in Instances)
{
if (instance.lobbyKey == lobbyKey)
{
throw new ArgumentException("The mod already has a settings snapshot.", "modId");
}
}
Instances.Add(this);
}
internal HostSetting<T> Register<T>(MelonPreferences_Entry<T> entry, T vanillaValue, Func<T, bool> validate = null, bool live = false, bool legacy = true, Func<T> localValue = null)
{
//IL_005b: Unknown result type (might be due to invalid IL or missing references)
//IL_0060: Unknown result type (might be due to invalid IL or missing references)
if (entry == null)
{
throw new ArgumentNullException("entry");
}
if (string.IsNullOrWhiteSpace(((MelonPreferences_Entry)entry).Identifier))
{
throw new ArgumentException("A setting identifier is required.", "entry");
}
if (((MelonPreferences_Entry)entry).Identifier.IndexOfAny(new char[3] { '\n', '\r', '\t' }) >= 0)
{
throw new ArgumentException("Setting identifiers cannot contain wire delimiters.", "entry");
}
if (lobby != CSteamID.Nil)
{
throw new InvalidOperationException("Register settings before entering a lobby.");
}
if (typeof(T) != typeof(bool) && typeof(T) != typeof(int) && typeof(T) != typeof(float) && typeof(T) != typeof(string))
{
throw new NotSupportedException("Host settings support bool, int, float and string values.");
}
if (legacy && typeof(T) == typeof(string))
{
throw new ArgumentException("String settings must use the extended snapshot.", "legacy");
}
foreach (ISetting setting in settings)
{
if (setting.Key == ((MelonPreferences_Entry)entry).Identifier)
{
throw new ArgumentException("Duplicate setting: " + ((MelonPreferences_Entry)entry).Identifier, "entry");
}
}
HostSetting<T> hostSetting = new HostSetting<T>(this, entry, vanillaValue, validate, live, legacy, localValue);
if (!hostSetting.IsValid(vanillaValue))
{
throw new ArgumentException("The vanilla value is invalid: " + ((MelonPreferences_Entry)entry).Identifier, "vanillaValue");
}
if (!hostSetting.IsValid(entry.DefaultValue))
{
throw new ArgumentException("The configured default is invalid: " + ((MelonPreferences_Entry)entry).Identifier, "entry");
}
settings.Add(hostSetting);
return hostSetting;
}
internal T GetValue<T>(string key, T localValue, T vanillaValue, bool live)
{
//IL_0001: Unknown result type (might be due to invalid IL or missing references)
//IL_0006: Unknown result type (might be due to invalid IL or missing references)
if (lobby == CSteamID.Nil)
{
return localValue;
}
if (live && !authorityValid)
{
return vanillaValue;
}
if (snapshot == null || !snapshot.TryGetValue(key, out var value))
{
return vanillaValue;
}
return (T)value;
}
internal static void Update()
{
//IL_0033: Unknown result type (might be due to invalid IL or missing references)
//IL_0038: Unknown result type (might be due to invalid IL or missing references)
if (Time.realtimeSinceStartup < nextPoll)
{
return;
}
nextPoll = Time.realtimeSinceStartup + 1f;
foreach (HostSettings instance in Instances)
{
if (instance.lobby != CSteamID.Nil)
{
instance.Refresh();
}
}
}
private void Begin(CSteamID newLobby, bool host)
{
//IL_000f: Unknown result type (might be due to invalid IL or missing references)
//IL_0014: Unknown result type (might be due to invalid IL or missing references)
//IL_0029: Unknown result type (might be due to invalid IL or missing references)
//IL_002e: Unknown result type (might be due to invalid IL or missing references)
//IL_003d: Unknown result type (might be due to invalid IL or missing references)
//IL_003e: Unknown result type (might be due to invalid IL or missing references)
//IL_0075: Unknown result type (might be due to invalid IL or missing references)
//IL_007a: Unknown result type (might be due to invalid IL or missing references)
//IL_0081: Unknown result type (might be due to invalid IL or missing references)
//IL_0086: Unknown result type (might be due to invalid IL or missing references)
//IL_008b: Unknown result type (might be due to invalid IL or missing references)
//IL_0090: Unknown result type (might be due to invalid IL or missing references)
//IL_0095: Unknown result type (might be due to invalid IL or missing references)
//IL_0097: Unknown result type (might be due to invalid IL or missing references)
//IL_009c: Unknown result type (might be due to invalid IL or missing references)
//IL_00c8: Unknown result type (might be due to invalid IL or missing references)
//IL_00cd: Unknown result type (might be due to invalid IL or missing references)
//IL_00ba: Unknown result type (might be due to invalid IL or missing references)
//IL_00bf: Unknown result type (might be due to invalid IL or missing references)
//IL_00ac: Unknown result type (might be due to invalid IL or missing references)
//IL_00b1: Unknown result type (might be due to invalid IL or missing references)
if (settings.Count == 0 || (lobby == newLobby && (!host || hostCreated)))
{
return;
}
if (lobby != newLobby)
{
Reset();
}
lobby = newLobby;
isHost = false;
hostCreated = host;
authorityValid = false;
ownerChanged = false;
snapshot = null;
published = false;
hostPayload = null;
owner = CSteamID.Nil;
try
{
owner = SteamMatchmaking.GetLobbyOwner(lobby);
CSteamID steamID = SteamUser.GetSteamID();
if (owner == CSteamID.Nil || (host ? (owner != steamID) : (owner == steamID)))
{
owner = CSteamID.Nil;
Warn("Lobby owner does not identify the expected host; using vanilla settings.");
Changed();
return;
}
isHost = host;
authorityValid = true;
if (host)
{
CaptureHostSnapshot();
}
else
{
CaptureClientSnapshot();
}
}
catch (Exception ex)
{
Warn("Initial lobby settings failed; vanilla settings are frozen for this session: " + ex.GetType().Name + ": " + ex.Message);
}
Refresh();
Changed();
}
private static void EnsureCallback()
{
if (dataCallback == null)
{
dataCallback = Callback<LobbyDataUpdate_t>.Create((DispatchDelegate<LobbyDataUpdate_t>)OnLobbyDataUpdated);
}
}
private static void OnLobbyDataUpdated(LobbyDataUpdate_t callback)
{
//IL_0020: Unknown result type (might be due to invalid IL or missing references)
//IL_0028: Unknown result type (might be due to invalid IL or missing references)
//IL_002e: Unknown result type (might be due to invalid IL or missing references)
//IL_0036: Unknown result type (might be due to invalid IL or missing references)
foreach (HostSettings instance in Instances)
{
if (instance.lobby.m_SteamID == callback.m_ulSteamIDLobby && callback.m_ulSteamIDMember == callback.m_ulSteamIDLobby)
{
if (callback.m_bSuccess == 0)
{
instance.Warn("Steam lobby data update failed; frozen session settings are retained.");
}
else
{
instance.Refresh();
}
}
}
}
private void CaptureHostSnapshot()
{
Dictionary<string, object> dictionary = new Dictionary<string, object>(StringComparer.Ordinal);
StringBuilder stringBuilder = new StringBuilder(schemaVersion.ToString(CultureInfo.InvariantCulture));
for (int i = 0; i < settings.Count; i++)
{
ISetting setting = settings[i];
object localValue = setting.LocalValue;
dictionary.Add(setting.Key, localValue);
if (setting.Legacy)
{
stringBuilder.Append('\n').Append(setting.Key).Append('\t')
.Append(Format(localValue));
}
}
string s = stringBuilder.ToString();
if (Encoding.UTF8.GetByteCount(s) + 1 > 8192)
{
throw new FormatException("Settings snapshot exceeds the Steam metadata limit.");
}
hostPayload = s;
snapshot = dictionary;
modernRevision = 1L;
modernPayload = BuildModern(dictionary, modernRevision);
}
private unsafe void CaptureClientSnapshot()
{
//IL_0001: Unknown result type (might be due to invalid IL or missing references)
//IL_0050: Unknown result type (might be due to invalid IL or missing references)
//IL_00ab: Unknown result type (might be due to invalid IL or missing references)
//IL_00b0: Unknown result type (might be due to invalid IL or missing references)
string lobbyData = SteamMatchmaking.GetLobbyData(lobby, modernKey);
if (!string.IsNullOrEmpty(lobbyData))
{
snapshot = ParseModern(lobbyData, out modernRevision);
lastModernPayload = lobbyData;
MelonLogger.Msg("[" + modId + "] Host settings captured; live values follow the host, lobby values remain fixed.");
return;
}
string lobbyData2 = SteamMatchmaking.GetLobbyData(lobby, lobbyKey);
if (string.IsNullOrEmpty(lobbyData2))
{
Warn("Host snapshot is absent; vanilla settings are frozen for this session.");
return;
}
snapshot = ParseSnapshot(lobbyData2);
legacyHost = true;
string[] obj = new string[5] { "[", modId, "] Host settings snapshot frozen for lobby ", null, null };
CSteamID val = lobby;
obj[3] = ((object)(*(CSteamID*)(&val))/*cast due to .constrained prefix*/).ToString();
obj[4] = ".";
MelonLogger.Msg(string.Concat(obj));
}
private void Refresh()
{
//IL_0008: Unknown result type (might be due to invalid IL or missing references)
//IL_000d: Unknown result type (might be due to invalid IL or missing references)
//IL_0012: Unknown result type (might be due to invalid IL or missing references)
//IL_0042: Unknown result type (might be due to invalid IL or missing references)
//IL_0043: Unknown result type (might be due to invalid IL or missing references)
//IL_001c: Unknown result type (might be due to invalid IL or missing references)
//IL_0021: Unknown result type (might be due to invalid IL or missing references)
//IL_004f: Unknown result type (might be due to invalid IL or missing references)
//IL_0051: Unknown result type (might be due to invalid IL or missing references)
//IL_002d: Unknown result type (might be due to invalid IL or missing references)
//IL_002f: Unknown result type (might be due to invalid IL or missing references)
bool flag = false;
try
{
EnsureCallback();
CSteamID lobbyOwner = SteamMatchmaking.GetLobbyOwner(lobby);
if (ownerChanged || owner == CSteamID.Nil || lobbyOwner != owner)
{
authorityValid = false;
if (lobbyOwner != CSteamID.Nil && lobbyOwner != owner)
{
ownerChanged = true;
}
Warn("Lobby owner changed or is unavailable; authority revoked and frozen session settings retained.");
return;
}
bool num = !authorityValid;
authorityValid = true;
flag = true;
if (num)
{
MelonLogger.Msg("[" + modId + "] Pinned lobby authority recovered; frozen settings retained.");
}
if (isHost)
{
if (hostPayload != null)
{
if (published)
{
RefreshHostLive();
}
else
{
Publish();
}
}
}
else
{
RefreshClientLive();
}
}
catch (Exception ex)
{
authorityValid = flag;
Warn("Refresh lobby settings failed; frozen session settings retained: " + ex.GetType().Name + ": " + ex.Message);
}
}
private unsafe void Publish()
{
//IL_0001: Unknown result type (might be due to invalid IL or missing references)
//IL_0062: Unknown result type (might be due to invalid IL or missing references)
//IL_001f: Unknown result type (might be due to invalid IL or missing references)
//IL_0086: Unknown result type (might be due to invalid IL or missing references)
//IL_008b: Unknown result type (might be due to invalid IL or missing references)
//IL_003e: Unknown result type (might be due to invalid IL or missing references)
//IL_0043: Unknown result type (might be due to invalid IL or missing references)
//IL_00e7: Unknown result type (might be due to invalid IL or missing references)
//IL_00ec: Unknown result type (might be due to invalid IL or missing references)
if (SteamMatchmaking.GetLobbyData(lobby, lobbyKey) != hostPayload && !SteamMatchmaking.SetLobbyData(lobby, lobbyKey, hostPayload))
{
CSteamID val = lobby;
Warn("SetLobbyData rejected the settings snapshot for lobby " + ((object)(*(CSteamID*)(&val))/*cast due to .constrained prefix*/).ToString() + ".");
}
else if (SteamMatchmaking.GetLobbyData(lobby, lobbyKey) != hostPayload)
{
CSteamID val = lobby;
Warn("Settings snapshot readback differs for lobby " + ((object)(*(CSteamID*)(&val))/*cast due to .constrained prefix*/).ToString() + ".");
}
else if (PublishModern(modernPayload))
{
published = true;
Recover();
string[] obj = new string[5] { "[", modId, "] Settings snapshot confirmed by local lobby readback for ", null, null };
CSteamID val = lobby;
obj[3] = ((object)(*(CSteamID*)(&val))/*cast due to .constrained prefix*/).ToString();
obj[4] = ".";
MelonLogger.Msg(string.Concat(obj));
}
}
private Dictionary<string, object> ParseSnapshot(string payload)
{
if (Encoding.UTF8.GetByteCount(payload) + 1 > 8192)
{
throw new FormatException("Settings snapshot exceeds the Steam metadata limit.");
}
string[] array = payload.Split('\n');
if (array[0] != schemaVersion.ToString(CultureInfo.InvariantCulture))
{
throw new FormatException("Settings snapshot schema mismatch.");
}
if (array.Length != settings.FindAll((ISetting s) => s.Legacy).Count + 1)
{
throw new FormatException("Settings snapshot has missing or extra keys.");
}
Dictionary<string, object> dictionary = new Dictionary<string, object>(StringComparer.Ordinal);
for (int num = 1; num < array.Length; num++)
{
string text = array[num];
int num2 = text.IndexOf('\t');
if (num2 <= 0 || num2 != text.LastIndexOf('\t') || text.IndexOf('\r') >= 0)
{
throw new FormatException("Settings snapshot has an invalid entry delimiter.");
}
string key = text.Substring(0, num2);
if (dictionary.ContainsKey(key))
{
throw new FormatException("Settings snapshot has a duplicate or missing key.");
}
ISetting setting = settings.Find((ISetting candidate) => candidate.Key == key && candidate.Legacy);
if (setting == null)
{
throw new FormatException("Settings snapshot has an unknown key: " + key);
}
dictionary.Add(key, setting.Parse(text.Substring(num2 + 1)));
}
return dictionary;
}
private string BuildModern(Dictionary<string, object> values, long revision)
{
StringBuilder stringBuilder = new StringBuilder("2\t" + revision.ToString(CultureInfo.InvariantCulture));
foreach (ISetting setting in settings)
{
stringBuilder.Append('\n').Append(setting.Key).Append('\t')
.Append(Format(values[setting.Key]));
}
string text = stringBuilder.ToString();
if (Encoding.UTF8.GetByteCount(text) + 1 > 8192)
{
throw new FormatException("Extended settings exceed the Steam metadata limit.");
}
return text;
}
private Dictionary<string, object> ParseModern(string payload, out long revision)
{
if (Encoding.UTF8.GetByteCount(payload) + 1 > 8192)
{
throw new FormatException("Extended settings exceed the Steam metadata limit.");
}
string[] array = payload.Split('\n');
string[] array2 = array[0].Split('\t');
if (array2.Length != 2 || array2[0] != "2" || !long.TryParse(array2[1], NumberStyles.None, CultureInfo.InvariantCulture, out revision) || revision < 1)
{
throw new FormatException("Invalid extended settings schema or revision.");
}
Dictionary<string, object> dictionary = new Dictionary<string, object>(StringComparer.Ordinal);
HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal);
for (int i = 1; i < array.Length; i++)
{
int num = array[i].IndexOf('\t');
if (num <= 0 || num != array[i].LastIndexOf('\t') || array[i].IndexOf('\r') >= 0)
{
throw new FormatException("Invalid extended settings delimiter.");
}
string key = array[i].Substring(0, num);
if (!hashSet.Add(key))
{
throw new FormatException("Duplicate extended setting: " + key);
}
ISetting setting = settings.Find((ISetting s) => s.Key == key);
if (setting != null)
{
dictionary.Add(key, setting.Parse(array[i].Substring(num + 1)));
}
}
return dictionary;
}
private bool PublishModern(string payload)
{
//IL_0001: Unknown result type (might be due to invalid IL or missing references)
//IL_003b: Unknown result type (might be due to invalid IL or missing references)
//IL_001a: Unknown result type (might be due to invalid IL or missing references)
if (SteamMatchmaking.GetLobbyData(lobby, modernKey) != payload && !SteamMatchmaking.SetLobbyData(lobby, modernKey, payload))
{
Warn("SetLobbyData rejected the extended settings; last confirmed values retained.");
return false;
}
if (SteamMatchmaking.GetLobbyData(lobby, modernKey) != payload)
{
Warn("Extended settings readback differs; last confirmed values retained.");
return false;
}
return true;
}
private void RefreshHostLive()
{
if (snapshot == null)
{
return;
}
Dictionary<string, object> dictionary = new Dictionary<string, object>(snapshot, StringComparer.Ordinal);
bool flag = false;
foreach (ISetting setting in settings)
{
if (setting.Live)
{
object localValue = setting.LocalValue;
if (!dictionary.TryGetValue(setting.Key, out var value) || !object.Equals(value, localValue))
{
dictionary[setting.Key] = localValue;
flag = true;
}
}
}
if (!flag)
{
if (PublishModern(modernPayload))
{
Recover();
}
return;
}
long revision = checked(modernRevision + 1);
string payload = BuildModern(dictionary, revision);
if (PublishModern(payload))
{
snapshot = dictionary;
modernRevision = revision;
modernPayload = payload;
Recover();
Changed(this.LiveValuesChanged);
}
}
private void RefreshClientLive()
{
//IL_0001: Unknown result type (might be due to invalid IL or missing references)
string lobbyData = SteamMatchmaking.GetLobbyData(lobby, modernKey);
if (string.IsNullOrEmpty(lobbyData) || lobbyData == lastModernPayload)
{
return;
}
long revision;
Dictionary<string, object> dictionary = ParseModern(lobbyData, out revision);
if (revision <= modernRevision)
{
Warn("Extended settings revision did not advance; last confirmed values retained.");
return;
}
Dictionary<string, object> dictionary2 = ((snapshot == null) ? new Dictionary<string, object>(StringComparer.Ordinal) : new Dictionary<string, object>(snapshot, StringComparer.Ordinal));
bool flag = false;
foreach (ISetting setting in settings)
{
if (setting.Live && dictionary.TryGetValue(setting.Key, out var value) && (!dictionary2.TryGetValue(setting.Key, out var value2) || !object.Equals(value2, value)))
{
dictionary2[setting.Key] = value;
flag = true;
}
}
snapshot = dictionary2;
modernRevision = revision;
lastModernPayload = lobbyData;
Recover();
if (flag)
{
Changed(this.LiveValuesChanged);
}
}
private static string Format(object value)
{
if (value is string s)
{
return "s:" + Convert.ToBase64String(Encoding.UTF8.GetBytes(s));
}
if (value is float num)
{
return num.ToString("R", CultureInfo.InvariantCulture);
}
if (value is bool)
{
if (!(bool)value)
{
return "false";
}
return "true";
}
return ((int)value).ToString(CultureInfo.InvariantCulture);
}
private void Warn(string reason)
{
if (warningReason == reason)
{
warningRepeats++;
return;
}
if (warningRepeats > 0)
{
MelonLogger.Warning("[" + modId + "] Previous settings warning repeated " + warningRepeats + " times.");
}
warningReason = reason;
warningRepeats = 0;
MelonLogger.Warning("[" + modId + "] " + reason);
}
private void Recover()
{
if (warningReason != null)
{
MelonLogger.Msg("[" + modId + "] Lobby settings recovered after " + (warningRepeats + 1) + " warning occurrences.");
warningReason = null;
warningRepeats = 0;
}
}
private void Changed()
{
Changed(this.EffectiveValuesChanged);
}
private void Changed(Action handlers)
{
if (handlers == null)
{
return;
}
Delegate[] invocationList = handlers.GetInvocationList();
for (int i = 0; i < invocationList.Length; i++)
{
Action action = (Action)invocationList[i];
try
{
action();
}
catch (Exception ex)
{
MelonLogger.Error("[" + modId + "] Apply effective settings failed.", ex);
}
}
}
private void Reset()
{
//IL_0001: Unknown result type (might be due to invalid IL or missing references)
//IL_0006: Unknown result type (might be due to invalid IL or missing references)
//IL_0011: Unknown result type (might be due to invalid IL or missing references)
//IL_0016: Unknown result type (might be due to invalid IL or missing references)
//IL_001c: Unknown result type (might be due to invalid IL or missing references)
//IL_0021: Unknown result type (might be due to invalid IL or missing references)
bool num = lobby != CSteamID.Nil;
lobby = CSteamID.Nil;
owner = CSteamID.Nil;
isHost = false;
hostCreated = false;
authorityValid = false;
ownerChanged = false;
published = false;
snapshot = null;
hostPayload = null;
modernPayload = null;
lastModernPayload = null;
modernRevision = 0L;
legacyHost = false;
warningReason = null;
warningRepeats = 0;
if (num)
{
MelonLogger.Msg("[" + modId + "] Session settings cleared; local settings restored.");
Changed();
}
}
}
internal interface ISetting
{
string Key { get; }
object LocalValue { get; }
bool Live { get; }
bool Legacy { get; }
bool IsValid(object value);
object Parse(string value);
}
internal sealed class HostSetting<T> : ISetting
{
private readonly HostSettings owner;
private readonly MelonPreferences_Entry<T> entry;
private readonly T vanillaValue;
private readonly Func<T, bool> validate;
private readonly Func<T> localValue;
private bool invalidLocal;
public bool Live { get; }
public bool Legacy { get; }
internal T Value => owner.GetValue(Key, (T)LocalValue, vanillaValue, Live);
public string Key => ((MelonPreferences_Entry)entry).Identifier;
public object LocalValue
{
get
{
T val = ((localValue == null) ? entry.Value : localValue());
if (IsValid(val))
{
if (invalidLocal)
{
MelonLogger.Msg("Local setting recovered: " + Key);
}
invalidLocal = false;
return val;
}
if (!invalidLocal)
{
MelonLogger.Warning("Invalid local setting " + Key + "; using its configured default without overwriting the file.");
}
invalidLocal = true;
return entry.DefaultValue;
}
}
internal HostSetting(HostSettings owner, MelonPreferences_Entry<T> entry, T vanillaValue, Func<T, bool> validate, bool live, bool legacy, Func<T> localValue)
{
this.owner = owner;
this.entry = entry;
this.vanillaValue = vanillaValue;
this.validate = validate;
this.localValue = localValue;
Live = live;
Legacy = legacy;
}
public bool IsValid(object value)
{
if (!(value is T arg))
{
return false;
}
if (value is float f && (float.IsNaN(f) || float.IsInfinity(f)))
{
return false;
}
if (value is string text && (Encoding.UTF8.GetByteCount(text) > 1024 || text.IndexOf('\0') >= 0))
{
return false;
}
if (validate != null)
{
return validate(arg);
}
return true;
}
public object Parse(string value)
{
object obj;
int result2;
float result3;
if (typeof(T) == typeof(bool) && bool.TryParse(value, out var result))
{
obj = result;
}
else if (typeof(T) == typeof(int) && int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out result2))
{
obj = result2;
}
else if (typeof(T) == typeof(float) && float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out result3))
{
obj = result3;
}
else
{
if (!(typeof(T) == typeof(string)) || !value.StartsWith("s:", StringComparison.Ordinal))
{
throw new FormatException("Invalid settings value: " + Key);
}
obj = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true).GetString(Convert.FromBase64String(value.Substring(2)));
}
if (!IsValid(obj))
{
throw new FormatException("Settings value is out of range: " + Key);
}
return obj;
}
}
internal static class ModVersion
{
internal const string Current = "1.6.0";
}
}
namespace BetterJump
{
public sealed class Core : MelonMod
{
public override void OnInitializeMelon()
{
BetterJumpPreferences.Initialize();
MelonLogger.Msg("BetterJump initialized. JumpVelocity={0:F2}, ForceUngroundTime={1:F2}, AirGravityScale={2:F2}", new object[3]
{
BetterJumpPreferences.JumpVelocity,
BetterJumpPreferences.ForceUngroundTime,
BetterJumpPreferences.AirGravityScale
});
}
public override void OnUpdate()
{
HostSettings.Update();
}
}
}
namespace BetterJump.Runtime
{
internal static class JumpRuntimeState
{
private sealed class ActorState
{
public float ForceUngroundUntil;
public float AirGravityUntil;
public float GroundedSince;
public float LastGravityTime;
}
private static readonly Dictionary<ProtoActor, ActorState> States = new Dictionary<ProtoActor, ActorState>();
internal static void OnJumpStarted(FakeJumper fakeJumper)
{
if (!BetterJumpPreferences.Enabled || fakeJumper == null)
{
return;
}
ProtoActor fieldValue = ReflectionHelper.GetFieldValue<ProtoActor>((object)fakeJumper, "owner");
if ((Object)(object)fieldValue == (Object)null)
{
return;
}
bool flag = fieldValue.AmIAvatar();
bool grounded = GetGrounded(fieldValue);
bool flag2 = States.ContainsKey(fieldValue);
if (BetterJumpPreferences.DebugLogs)
{
MelonLogger.Msg("[DBG] StartJump trigger: avatar={0} grounded={1} inProgress={2} y={3:F2} falling={4:F2}", new object[5]
{
flag,
grounded,
flag2,
GetHeight(fieldValue),
GetFalling(fieldValue)
});
}
if (flag && grounded && !flag2)
{
if (BetterJumpPreferences.DebugLogs)
{
MelonLogger.Msg("[DBG] -> BOOST falling={0:F2}", new object[1] { BetterJumpPreferences.JumpVelocity });
}
SetFalling(fieldValue, BetterJumpPreferences.JumpVelocity);
SetGrounded(fieldValue, value: false);
SetWasGrounded(fieldValue, value: false);
ActorState value = new ActorState
{
ForceUngroundUntil = Time.time + Mathf.Max(0.02f, BetterJumpPreferences.ForceUngroundTime),
AirGravityUntil = Time.time + BetterJumpPreferences.MaxAirGravitySeconds,
GroundedSince = -1f,
LastGravityTime = -1f
};
States[fieldValue] = value;
}
}
internal static void OnGroundCheck(ProtoActor actor)
{
if (!BetterJumpPreferences.Enabled || !States.TryGetValue(actor, out var value))
{
return;
}
if (BetterJumpPreferences.DebugLogs)
{
string text = ((Time.time < value.ForceUngroundUntil) ? "FORCE" : (GetGrounded(actor) ? "GROUND" : "AIR"));
MelonLogger.Msg("[DBG] tick {0}: y={1:F2} falling={2:F2} grounded={3} wasGrounded={4} ground={5} t={6:F2}", new object[7]
{
text,
GetHeight(actor),
GetFalling(actor),
GetGrounded(actor),
GetWasGrounded(actor),
GroundProbe(actor),
Time.time
});
}
if (Time.time < value.ForceUngroundUntil)
{
SetGrounded(actor, value: false);
SetWasGrounded(actor, value: false);
value.GroundedSince = -1f;
ApplyAirGravity(actor, value);
}
else if (GetGrounded(actor))
{
if (value.GroundedSince < 0f)
{
value.GroundedSince = Time.time;
}
if (Time.time - value.GroundedSince >= BetterJumpPreferences.LandSettleSeconds)
{
States.Remove(actor);
}
}
else
{
value.GroundedSince = -1f;
if (Time.time >= value.AirGravityUntil)
{
States.Remove(actor);
}
else
{
ApplyAirGravity(actor, value);
}
}
}
internal static void Reset()
{
States.Clear();
}
internal static void OnActorDestroyed(ProtoActor actor)
{
States.Remove(actor);
}
private static bool GetGrounded(ProtoActor actor)
{
object propertyValue = ReflectionHelper.GetPropertyValue((object)actor, "grounded");
if (propertyValue != null && propertyValue is bool)
{
return (bool)propertyValue;
}
propertyValue = ReflectionHelper.GetFieldValue((object)actor, "<grounded>k__BackingField");
if (propertyValue != null && propertyValue is bool)
{
return (bool)propertyValue;
}
return false;
}
private static void SetGrounded(ProtoActor actor, bool value)
{
ReflectionHelper.SetFieldValue((object)actor, "<grounded>k__BackingField", (object)value);
}
private static void SetFalling(ProtoActor actor, float value)
{
ReflectionHelper.SetFieldValue((object)actor, "falling", (object)value);
}
private static void SetWasGrounded(ProtoActor actor, bool value)
{
ReflectionHelper.SetFieldValue((object)actor, "wasGrounded", (object)value);
}
private static float GetFalling(ProtoActor actor)
{
return ReflectionHelper.GetFieldValue<float>((object)actor, "falling");
}
private static bool GetWasGrounded(ProtoActor actor)
{
return ReflectionHelper.GetFieldValue<bool>((object)actor, "wasGrounded");
}
private static float GetHeight(ProtoActor actor)
{
//IL_0015: Unknown result type (might be due to invalid IL or missing references)
if (!((Object)(object)actor != (Object)null))
{
return 0f;
}
return ((Component)actor).transform.position.y;
}
private static string GroundProbe(ProtoActor actor)
{
//IL_0015: Unknown result type (might be due to invalid IL or missing references)
//IL_001a: Unknown result type (might be due to invalid IL or missing references)
//IL_0024: Unknown result type (might be due to invalid IL or missing references)
//IL_0029: Unknown result type (might be due to invalid IL or missing references)
//IL_002e: Unknown result type (might be due to invalid IL or missing references)
if ((Object)(object)actor == (Object)null)
{
return "n/a";
}
RaycastHit val = default(RaycastHit);
if (Physics.Raycast(((Component)actor).transform.position + Vector3.up * 0.1f, Vector3.down, ref val, 20f, -1, (QueryTriggerInteraction)1))
{
string arg = LayerMask.LayerToName(((Component)((RaycastHit)(ref val)).collider).gameObject.layer);
return $"{((RaycastHit)(ref val)).distance - 0.1f:F2}m@{((Object)((RaycastHit)(ref val)).collider).name}[{arg}]";
}
return "none";
}
private static void ApplyAirGravity(ProtoActor actor, ActorState state)
{
//IL_0052: Unknown result type (might be due to invalid IL or missing references)
float airGravityScale = BetterJumpPreferences.AirGravityScale;
if (!(airGravityScale >= 1f))
{
float falling = GetFalling(actor);
if (!(falling <= 0f) && state.LastGravityTime != Time.time)
{
state.LastGravityTime = Time.time;
float num = Mathf.Clamp01(falling / BetterJumpPreferences.ApexBlendSpeed);
float num2 = Mathf.Lerp(1f, airGravityScale, num);
float num3 = Physics.gravity.y * Time.deltaTime * (num2 - 1f);
SetFalling(actor, falling + num3);
}
}
}
}
}
namespace BetterJump.Patches
{
[HarmonyPatch(typeof(FakeJumper), "StartJump")]
internal static class FakeJumperPatches
{
private static void Postfix(FakeJumper __instance)
{
JumpRuntimeState.OnJumpStarted(__instance);
}
}
[HarmonyPatch(typeof(ProtoActor), "CheckGrounded")]
internal static class ProtoActorGroundedPatch
{
private static void Postfix(ProtoActor __instance)
{
JumpRuntimeState.OnGroundCheck(__instance);
}
}
[HarmonyPatch(typeof(ProtoActor), "OnDestroy")]
internal static class ProtoActorDestroyPatch
{
private static void Postfix(ProtoActor __instance)
{
JumpRuntimeState.OnActorDestroyed(__instance);
}
}
}
namespace BetterJump.Config
{
internal static class BetterJumpPreferences
{
private const string CategoryId = "BetterJump";
private static MelonPreferences_Category _category;
private static MelonPreferences_Entry<bool> _enabled;
private static MelonPreferences_Entry<float> _jumpVelocity;
private static MelonPreferences_Entry<float> _forceUngroundTime;
private static MelonPreferences_Entry<float> _airGravityScale;
private static MelonPreferences_Entry<bool> _debugLogs;
private static HostSettings _hostSettings;
private static HostSetting<bool> _hostEnabled;
private static HostSetting<float> _hostJumpVelocity;
private static HostSetting<float> _hostForceUngroundTime;
private static HostSetting<float> _hostAirGravityScale;
private static HostSetting<float> _landSettleSeconds;
private static HostSetting<float> _apexBlendSpeed;
private static HostSetting<float> _maxAirGravitySeconds;
internal static bool Enabled => _hostEnabled?.Value ?? false;
internal static float JumpVelocity => Mathf.Max(0f, _hostJumpVelocity?.Value ?? 0f);
internal static float ForceUngroundTime => Mathf.Max(0.01f, _hostForceUngroundTime?.Value ?? 0.01f);
internal static float AirGravityScale => Mathf.Clamp(_hostAirGravityScale?.Value ?? 1f, 0.1f, 1f);
internal static bool DebugLogs => _debugLogs.Value;
internal static float LandSettleSeconds => _landSettleSeconds?.Value ?? 0.15f;
internal static float ApexBlendSpeed => _apexBlendSpeed?.Value ?? 2.5f;
internal static float MaxAirGravitySeconds => _maxAirGravitySeconds?.Value ?? 8f;
internal static void Initialize()
{
if (_category == null)
{
_category = MelonPreferences.CreateCategory("BetterJump", "BetterJump");
_enabled = CreateEntry("Enabled", defaultValue: true, "Enabled", "Enable BetterJump functionality. When disabled, the mod will not modify jump behavior.");
_jumpVelocity = CreateEntry("JumpVelocity", 4.8f, "Jump Velocity", "Upward speed applied when a jump starts (units/second). Higher values result in stronger jumps. Default: 4.8");
_forceUngroundTime = CreateEntry("ForceUngroundTime", 0.08f, "Force Unground Time", "Seconds to keep avatar airborne before the next ground check can succeed. This prevents the game from immediately detecting the ground after a jump, allowing for better jump responsiveness. Default: 0.08");
_airGravityScale = CreateEntry("AirGravityScale", 0.7f, "Air Gravity Scale", "Gravity multiplier applied during the fast part of the RISE (lower = floatier, higher arc). Near the apex it blends back to normal gravity so there is no hovering 'platform' at the top, and the descent always uses normal gravity for crisp landings. 1.0 disables the effect. Default: 0.7");
_debugLogs = CreateEntry("DebugLogs", defaultValue: false, "Debug Logs", "Enable detailed debug logging for troubleshooting jump behavior.");
_hostSettings = new HostSettings("BetterJump");
_hostEnabled = _hostSettings.Register(_enabled, vanillaValue: false, null, live: true);
_hostJumpVelocity = _hostSettings.Register(_jumpVelocity, 0f, (float value) => value >= 0f, live: true, legacy: true, () => Mathf.Max(0f, _jumpVelocity.Value));
_hostForceUngroundTime = _hostSettings.Register(_forceUngroundTime, 0.01f, (float value) => value >= 0.01f, live: true, legacy: true, () => Mathf.Max(0.01f, _forceUngroundTime.Value));
_hostAirGravityScale = _hostSettings.Register(_airGravityScale, 1f, (float value) => value >= 0.1f && value <= 1f, live: true, legacy: true, () => Mathf.Clamp(_airGravityScale.Value, 0.1f, 1f));
_landSettleSeconds = _hostSettings.Register(CreateEntry("LandSettleSeconds", 0.15f, "Landing Settle Time", "Host setting, live. Continuous ground contact required before another boosted jump. Seconds, at least 0.01. Default: 0.15"), 0.15f, (float value) => value >= 0.01f, live: true, legacy: false);
_apexBlendSpeed = _hostSettings.Register(CreateEntry("ApexBlendSpeed", 2.5f, "Apex Blend Speed", "Host setting, live. Rising speed below which gravity blends back to normal. Positive units/second. Default: 2.5"), 2.5f, (float value) => value > 0f, live: true, legacy: false);
_maxAirGravitySeconds = _hostSettings.Register(CreateEntry("MaxAirGravitySeconds", 8f, "Air Gravity Timeout", "Host setting, live for the next jump. Maximum time before reduced gravity is released. Positive seconds. Default: 8"), 8f, (float value) => value > 0f, live: true, legacy: false);
_hostSettings.EffectiveValuesChanged += JumpRuntimeState.Reset;
_hostSettings.LiveValuesChanged += JumpRuntimeState.Reset;
}
}
private static MelonPreferences_Entry<T> CreateEntry<T>(string identifier, T defaultValue, string displayName, string description = null)
{
if (_category == null)
{
throw new InvalidOperationException("Preference category not initialized.");
}
return _category.CreateEntry<T>(identifier, defaultValue, displayName, description, false, false, (ValueValidator)null, (string)null);
}
}
}