Some mods target the Mono version of the game, which is available by opting into the Steam beta branch "alternate"
Decompiled source of Siesta v1.0.0
Siesta.dll
Decompiled 10 hours agousing System; using System.Collections.Generic; using System.Diagnostics; 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 HarmonyLib; using Il2CppInterop.Runtime.InteropTypes; using Il2CppScheduleOne.DevUtilities; using Il2CppScheduleOne.Economy; using Il2CppScheduleOne.Employees; using Il2CppScheduleOne.NPCs; using Il2CppScheduleOne.NPCs.Behaviour; using Il2CppScheduleOne.Networking; using Il2CppScheduleOne.PlayerScripts; using Il2CppSystem.Collections.Generic; using MelonLoader; using MelonLoader.Preferences; using Microsoft.CodeAnalysis; using S1API.Lifecycle; using Siesta; using Siesta.Compat; using Siesta.Config; using Siesta.Lod; using Siesta.UI; using UnityEngine; using UnityEngine.AI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: MelonInfo(typeof(Core), "Siesta", "1.0.0", "DooDesch", "https://github.com/DooDesch/ScheduleOne-Siesta")] [assembly: MelonGame("TVGS", "Schedule I")] [assembly: MelonOptionalDependencies(new string[] { "ModManager&PhoneApp" })] [assembly: TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName = ".NET 6.0")] [assembly: AssemblyCompany("Siesta")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0+5262514cead1cb81621057a8fb8c2e6329f883d8")] [assembly: AssemblyProduct("Siesta")] [assembly: AssemblyTitle("Siesta")] [assembly: NeutralResourcesLanguage("en-US")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.0.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 Siesta { public sealed class Core : MelonMod { private bool _inWorld; private float _teleElapsed; private int _teleFrames; private float _teleMaxDt; public static Core Instance { get; private set; } public static Instance Log { get; private set; } public override void OnInitializeMelon() { Instance = this; Log = ((MelonBase)this).LoggerInstance; Preferences.Initialize(); if (Preferences.MoreNpcsCompat) { MoreNpcsCompat.Apply(((MelonBase)this).HarmonyInstance); } GameLifecycle.OnSaveStart += delegate { LodController.RestoreAll("save starting"); }; GameLifecycle.OnPreSceneChange += delegate { LodController.RestoreAll("scene changing"); }; Log.Msg("Siesta v1.0.0 - NPC LOD active."); } public override void OnSceneWasLoaded(int buildIndex, string sceneName) { _inWorld = sceneName == "Main"; } public override void OnSceneWasUnloaded(int buildIndex, string sceneName) { _inWorld = false; LodController.Reset(); } public override void OnUpdate() { if (_inWorld) { LodController.Tick(); TelemetryTick(); } } public override void OnGUI() { if (_inWorld && Preferences.ShowFpsCounter) { FpsCounter.Draw(); } } public override void OnApplicationQuit() { LodController.RestoreAll("application quit"); } public override void OnDeinitializeMelon() { LodController.RestoreAll("melon unload"); } private void TelemetryTick() { float unscaledDeltaTime = Time.unscaledDeltaTime; _teleElapsed += unscaledDeltaTime; _teleFrames++; if (unscaledDeltaTime > _teleMaxDt) { _teleMaxDt = unscaledDeltaTime; } if (_teleElapsed < 15f) { return; } try { float value = (float)_teleFrames / _teleElapsed; float value2 = ((_teleMaxDt > 0f) ? (1f / _teleMaxDt) : 0f); LodRegistry.CountByTier(out var full, out var cosmetic, out var deep); Log.Msg($"[telemetry] fps={value:F0} (min {value2:F0}) npcs={full + cosmetic + deep} full={full} cosmetic={cosmetic} deep={deep}"); } catch { } finally { _teleElapsed = 0f; _teleFrames = 0; _teleMaxDt = 0f; } } } } namespace Siesta.UI { internal static class FpsCounter { private static GUIStyle _style; internal static void Draw() { //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Invalid comparison between Unknown and I4 //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_0049: 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_0058: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Expected O, but got Unknown //IL_0095: 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_0119: Unknown result type (might be due to invalid IL or missing references) //IL_0129: Unknown result type (might be due to invalid IL or missing references) //IL_0133: Unknown result type (might be due to invalid IL or missing references) //IL_008e: Unknown result type (might be due to invalid IL or missing references) //IL_0087: Unknown result type (might be due to invalid IL or missing references) if (Event.current == null || (int)Event.current.type == 7) { float smoothDeltaTime = Time.smoothDeltaTime; int num = ((smoothDeltaTime > 0f) ? Mathf.RoundToInt(1f / smoothDeltaTime) : 0); if (_style == null) { _style = new GUIStyle(GUI.skin.label) { fontSize = 22, fontStyle = (FontStyle)1, alignment = (TextAnchor)2 }; } _style.normal.textColor = (Color)((num >= 50) ? Color.green : ((num >= 30) ? Color.yellow : new Color(1f, 0.45f, 0.45f))); string text = num + " FPS"; Rect val = default(Rect); ((Rect)(ref val))..ctor((float)Screen.width - 140f, 6f, 132f, 28f); GUI.color = new Color(0f, 0f, 0f, 0.6f); GUI.Label(new Rect(((Rect)(ref val)).x + 1f, ((Rect)(ref val)).y + 1f, ((Rect)(ref val)).width, ((Rect)(ref val)).height), text, _style); GUI.color = Color.white; GUI.Label(val, text, _style); } } } } namespace Siesta.Lod { internal static class Exemptions { internal static bool ExemptOnAnyBehaviour; internal static bool IsDeepCullExempt(NPC npc, NpcModState st) { return Reason(npc, st) != null; } internal static string Reason(NPC npc, NpcModState st) { try { if (!npc.IsConscious) { return "unconscious"; } if (npc.IsInVehicle) { return "in-vehicle"; } if (npc.IsPanicked || npc.isUnsettled) { return "panicked"; } if (npc.IsImportant) { return "important"; } if (ExemptOnAnyBehaviour) { NPCBehaviour behaviour = npc.Behaviour; if ((Object)(object)behaviour != (Object)null) { if ((Object)(object)behaviour.activeBehaviour != (Object)null) { return "active-behaviour"; } List<Behaviour> behaviourStack = behaviour.behaviourStack; if (behaviourStack != null && behaviourStack.Count > 0) { return "behaviour-stack"; } } } if ((Object)(object)st.Dealer != (Object)null) { if ((Object)(object)st.Dealer.currentContract != (Object)null) { return "dealer-contract"; } DealerAttendDealBehaviour attendDealBehaviour = st.Dealer._attendDealBehaviour; if ((Object)(object)attendDealBehaviour != (Object)null && ((Behaviour)attendDealBehaviour).Active) { return "dealer-attend"; } } if ((Object)(object)st.Employee != (Object)null) { if (st.Employee.IsAnyWorkInProgress()) { return "employee-working"; } if (!st.Employee.ShouldIdle() && !st.Employee.IsWaitingOutside) { return "employee-busy"; } } if ((Object)(object)st.Customer != (Object)null) { if ((Object)(object)st.Customer.CurrentContract != (Object)null) { return "customer-contract"; } if (st.Customer.IsAwaitingDelivery) { return "customer-awaiting"; } } return null; } catch (Exception ex) { return "ex:" + ex.Message; } } } internal static class LodController { internal enum Control { Auto, ForceFull, ForceCosmetic, ForceDeep } private static int _cursor; private static readonly HashSet<int> _failed = new HashSet<int>(); internal static Control Mode = Control.Auto; private static Vector3[] _players = (Vector3[])(object)new Vector3[8]; private static int _playerCount; private static Camera _cam; private static float _camRefreshAt; internal static void Tick() { if (!Preferences.EnableLod) { if (LodRegistry.HasAny) { RestoreAll("LOD disabled"); } return; } if (Net.IsMultiplayer() && !Preferences.EnableInMultiplayer) { if (LodRegistry.HasAny) { RestoreAll("multiplayer + EnableInMultiplayer off"); } return; } if (Mode != Control.Auto) { ForceAll((Mode != Control.ForceFull) ? ((Mode == Control.ForceCosmetic) ? LodState.Cosmetic : LodState.Deep) : LodState.Full); return; } List<NPC> nPCRegistry = NPCManager.NPCRegistry; if (nPCRegistry == null) { return; } int count; try { count = nPCRegistry.Count; } catch { return; } if (count == 0) { return; } SnapshotPlayers(); if (_playerCount == 0) { return; } RefreshCamera(); bool authoritative = Net.IsAuthoritative(); int num = Math.Min(Preferences.BudgetPerFrame, count); for (int i = 0; i < num; i++) { if (_cursor >= count) { _cursor = 0; } NPC val; try { val = nPCRegistry[_cursor]; } catch { _cursor++; continue; } _cursor++; if (!((Object)(object)val == (Object)null)) { Evaluate(val, authoritative); } } } private static void Evaluate(NPC npc, bool authoritative) { //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_004a: 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) int instanceID; try { instanceID = ((Object)npc).GetInstanceID(); } catch { return; } NpcModState orAdd = LodRegistry.GetOrAdd(instanceID, npc); if (_failed.Contains(instanceID)) { if (orAdd.Tier != LodState.Full) { LodLevers.ForceFull(npc, orAdd); } return; } orAdd.Resolve(npc); Vector3 centerPoint; try { centerPoint = npc.CenterPoint; } catch { return; } float d = MinSqrDistToPlayer(centerPoint); bool onScreen = IsOnScreen(centerPoint); LodState lodState = Decide(npc, orAdd, d, onScreen, authoritative); if (lodState == orAdd.Tier) { return; } try { LodLevers.ApplyTier(npc, orAdd, lodState, authoritative); if (orAdd.WakeFailed) { orAdd.WakeFailed = false; _failed.Add(instanceID); Instance log = Core.Log; if (log != null) { log.Warning($"[Siesta] NPC {instanceID} wake failed - forcing permanent exempt (kept Full)."); } LodLevers.ForceFull(npc, orAdd); } } catch (Exception ex) { Instance log2 = Core.Log; if (log2 != null) { log2.Warning("[Siesta] ApplyTier failed: " + ex.Message); } } } private static LodState Decide(NPC npc, NpcModState st, float d2, bool onScreen, bool authoritative) { if (onScreen && Preferences.RespectOnScreen) { return LodState.Full; } float cosmeticDistance = Preferences.CosmeticDistance; float deepDistance = Preferences.DeepDistance; float hysteresis = Preferences.Hysteresis; float num = cosmeticDistance * cosmeticDistance; float num2 = (cosmeticDistance + hysteresis) * (cosmeticDistance + hysteresis); float num3 = deepDistance * deepDistance; float num4 = (deepDistance + hysteresis) * (deepDistance + hysteresis); LodState lodState = st.Tier switch { LodState.Full => (d2 >= num4) ? LodState.Deep : ((d2 >= num2) ? LodState.Cosmetic : LodState.Full), LodState.Cosmetic => (!(d2 < num)) ? ((!(d2 >= num4)) ? LodState.Cosmetic : LodState.Deep) : LodState.Full, _ => (!(d2 < num)) ? ((d2 < num3) ? LodState.Cosmetic : LodState.Deep) : LodState.Full, }; if (lodState == LodState.Deep) { string text = (st.ExemptReason = ((!authoritative) ? "not-host" : Exemptions.Reason(npc, st))); if (!Preferences.UseDeepCull || text != null) { lodState = (Preferences.UseCosmeticCull ? LodState.Cosmetic : LodState.Full); } } if (lodState == LodState.Cosmetic && !Preferences.UseCosmeticCull) { lodState = LodState.Full; } return lodState; } private static float MinSqrDistToPlayer(Vector3 p) { //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0015: 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_001b: Unknown result type (might be due to invalid IL or missing references) float num = float.MaxValue; for (int i = 0; i < _playerCount; i++) { Vector3 val = _players[i] - p; float sqrMagnitude = ((Vector3)(ref val)).sqrMagnitude; if (sqrMagnitude < num) { num = sqrMagnitude; } } return num; } private static void SnapshotPlayers() { //IL_0088: Unknown result type (might be due to invalid IL or missing references) //IL_0048: Unknown result type (might be due to invalid IL or missing references) _playerCount = 0; try { List<Player> playerList = Player.PlayerList; if (playerList != null && playerList.Count > 0) { int count = playerList.Count; for (int i = 0; i < count; i++) { Player val = playerList[i]; if (!((Object)(object)val == (Object)null)) { Transform transform = ((Component)val).transform; if (!((Object)(object)transform == (Object)null)) { Add(transform.position); } } } } if (_playerCount == 0) { Player local = Player.Local; if ((Object)(object)local != (Object)null && (Object)(object)((Component)local).transform != (Object)null) { Add(((Component)local).transform.position); } } } catch { } } private static void Add(Vector3 v) { //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Unknown result type (might be due to invalid IL or missing references) if (_playerCount < _players.Length) { _players[_playerCount++] = v; } } private static void RefreshCamera() { if (!((Object)(object)_cam != (Object)null) || !(Time.unscaledTime < _camRefreshAt)) { try { _cam = Camera.main; } catch { _cam = null; } _camRefreshAt = Time.unscaledTime + 2f; } } private static bool IsOnScreen(Vector3 p) { //IL_0019: 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) //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_0041: 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) Camera cam = _cam; if ((Object)(object)cam == (Object)null) { return false; } try { Transform transform = ((Component)cam).transform; Vector3 val = p - transform.position; float magnitude = ((Vector3)(ref val)).magnitude; if (magnitude < 0.001f) { return true; } return Vector3.Dot(transform.forward, val / magnitude) > 0.5f; } catch { return false; } } internal static void RestoreAll(string reason) { try { LodRegistry.RestoreAll(); _failed.Clear(); _cursor = 0; } catch (Exception ex) { Instance log = Core.Log; if (log != null) { log.Warning("[Siesta] RestoreAll failed: " + ex.Message); } } } internal static void Reset() { LodRegistry.Reset(); _failed.Clear(); _cursor = 0; _cam = null; } internal static void ForceAll(LodState target) { List<NPC> nPCRegistry = NPCManager.NPCRegistry; if (nPCRegistry == null) { return; } int count; try { count = nPCRegistry.Count; } catch { return; } bool flag = Net.IsAuthoritative(); for (int i = 0; i < count; i++) { NPC val; try { val = nPCRegistry[i]; } catch { continue; } if ((Object)(object)val == (Object)null) { continue; } int instanceID; try { instanceID = ((Object)val).GetInstanceID(); } catch { continue; } if (!_failed.Contains(instanceID)) { NpcModState orAdd = LodRegistry.GetOrAdd(instanceID, val); orAdd.Resolve(val); LodState lodState = target; if (lodState == LodState.Deep && (orAdd.ExemptReason = ((!flag) ? "not-host" : Exemptions.Reason(val, orAdd))) != null) { lodState = LodState.Cosmetic; } try { LodLevers.ApplyTier(val, orAdd, lodState, flag); } catch { } } } } } internal static class LodLevers { internal static void ApplyTier(NPC npc, NpcModState st, LodState target, bool authoritative) { if (target == st.Tier) { return; } switch (target) { case LodState.Full: if (st.DeepApplied) { RestoreDeep(npc, st); } if (st.Hidden) { Show(npc, st); } break; case LodState.Cosmetic: if (st.DeepApplied) { RestoreDeep(npc, st); } if (!st.Hidden) { Hide(npc, st); } break; case LodState.Deep: if (!st.Hidden) { Hide(npc, st); } if (!st.DeepApplied) { ApplyDeep(npc, st, authoritative); } break; } st.Tier = target; } internal static void ForceFull(NPC npc, NpcModState st) { try { if (st.DeepApplied) { RestoreDeep(npc, st); } if (st.Hidden) { Show(npc, st); } st.Tier = LodState.Full; } catch (Exception ex) { Instance log = Core.Log; if (log != null) { log.Warning("[Siesta] ForceFull failed: " + ex.Message); } } } private static void Hide(NPC npc, NpcModState st) { npc.SetVisible(false, false); st.Hidden = true; } private static void Show(NPC npc, NpcModState st) { npc.SetVisible(true, false); st.Hidden = false; } private static void ApplyDeep(NPC npc, NpcModState st, bool authoritative) { if (!authoritative) { Instance log = Core.Log; if (log != null) { log.Warning($"[Siesta] ERROR client-attempted-deepcull npc={st.Id} (ignored)"); } return; } NPCMovement movement = npc.Movement; if ((Object)(object)movement != (Object)null && !movement.IsPaused) { movement.PauseMovement(); st.WePausedMovement = true; } NPCScheduleManager schedule = st.Schedule; if ((Object)(object)schedule != (Object)null && schedule.ScheduleEnabled) { schedule.DisableSchedule(); st.WeDisabledSchedule = true; } st.DeepApplied = true; } private static void RestoreDeep(NPC npc, NpcModState st) { NPCScheduleManager schedule = st.Schedule; if (st.WeDisabledSchedule && (Object)(object)schedule != (Object)null) { schedule.EnableSchedule(); schedule.EnforceState(false); st.WeDisabledSchedule = false; } NPCMovement movement = npc.Movement; if (st.WePausedMovement && (Object)(object)movement != (Object)null) { movement.ResumeMovement(); st.WePausedMovement = false; if (!RepairNavMesh(npc, movement, st)) { st.WakeFailed = true; } } st.DeepApplied = false; } private static bool RepairNavMesh(NPC npc, NPCMovement mv, NpcModState st) { //IL_0091: 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_00ad: Unknown result type (might be due to invalid IL or missing references) try { NavMeshAgent agent = mv.Agent; if ((Object)(object)agent == (Object)null) { Instance log = Core.Log; if (log != null) { log.Warning($"[Siesta] WAKE-FAILED npc={st.Id} reason=agent-null"); } return false; } if (!((Behaviour)agent).enabled) { mv.SetAgentEnabled(true); } if (agent.isOnNavMesh) { return true; } mv.WarpToNavMesh(); if (agent.isOnNavMesh) { return true; } NavMeshHit val = default(NavMeshHit); if (mv.SmartSampleNavMesh(mv.FootPosition, ref val, 1f, 10f, 3)) { mv.Warp(((NavMeshHit)(ref val)).position); if (agent.isOnNavMesh) { return true; } } mv.Warp(mv.FootPosition); if (agent.isOnNavMesh) { return true; } Instance log2 = Core.Log; if (log2 != null) { log2.Warning($"[Siesta] WAKE-FAILED npc={st.Id} reason=still-off-navmesh"); } return false; } catch (Exception ex) { Instance log3 = Core.Log; if (log3 != null) { log3.Warning($"[Siesta] WAKE-FAILED npc={st.Id} reason=exception {ex.Message}"); } return false; } } } internal static class LodRegistry { private static readonly Dictionary<int, NpcModState> _states = new Dictionary<int, NpcModState>(); internal static bool HasAny => _states.Count > 0; internal static int Count => _states.Count; internal static NpcModState GetOrAdd(int id, NPC npc) { if (!_states.TryGetValue(id, out var value)) { value = new NpcModState(id, npc); _states[id] = value; } else { value.Npc = npc; } return value; } internal static void RestoreAll() { foreach (KeyValuePair<int, NpcModState> state in _states) { NpcModState value = state.Value; try { if ((Object)(object)value.Npc != (Object)null) { LodLevers.ForceFull(value.Npc, value); } } catch { } } _states.Clear(); } internal static void Reset() { _states.Clear(); } internal static string ReasonTally() { Dictionary<string, int> dictionary = new Dictionary<string, int>(); foreach (KeyValuePair<int, NpcModState> state in _states) { string key = state.Value.ExemptReason ?? "ELIGIBLE"; dictionary.TryGetValue(key, out var value); dictionary[key] = value + 1; } StringBuilder stringBuilder = new StringBuilder(); foreach (KeyValuePair<string, int> item in dictionary) { stringBuilder.Append(item.Key).Append('=').Append(item.Value) .Append(" "); } return stringBuilder.ToString().Trim(); } internal static void CountByTier(out int full, out int cosmetic, out int deep) { full = 0; cosmetic = 0; deep = 0; using Dictionary<int, NpcModState>.Enumerator enumerator = _states.GetEnumerator(); while (enumerator.MoveNext()) { switch (enumerator.Current.Value.Tier) { case LodState.Full: full++; break; case LodState.Cosmetic: cosmetic++; break; default: deep++; break; } } } } internal enum LodState { Full, Cosmetic, Deep } internal sealed class NpcModState { internal readonly int Id; internal NPC Npc; internal LodState Tier; internal bool Hidden; internal bool DeepApplied; internal bool WePausedMovement; internal bool WeDisabledSchedule; internal bool WakeFailed; internal string ExemptReason; private bool _resolved; internal NPCScheduleManager Schedule; internal Dealer Dealer; internal Employee Employee; internal Customer Customer; internal NpcModState(int id, NPC npc) { Id = id; Npc = npc; } internal void Resolve(NPC npc) { if (_resolved) { return; } _resolved = true; try { Schedule = ((Component)npc).GetComponentInChildren<NPCScheduleManager>(true); } catch { } try { Dealer = ((Il2CppObjectBase)npc).TryCast<Dealer>(); } catch { } try { Employee = ((Il2CppObjectBase)npc).TryCast<Employee>(); } catch { } try { Customer = ((Component)npc).GetComponent<Customer>(); } catch { } } } } namespace Siesta.Config { internal static class Preferences { private const string CategoryId = "Siesta_01_Main"; private static MelonPreferences_Category _category; private static MelonPreferences_Entry<bool> _enableLod; private static MelonPreferences_Entry<bool> _enableInMp; private static MelonPreferences_Entry<float> _cosmeticDistance; private static MelonPreferences_Entry<float> _deepDistance; private static MelonPreferences_Entry<float> _hysteresis; private static MelonPreferences_Entry<int> _budgetPerFrame; private static MelonPreferences_Entry<bool> _useCosmeticCull; private static MelonPreferences_Entry<bool> _useDeepCull; private static MelonPreferences_Entry<bool> _respectOnScreen; private static MelonPreferences_Entry<bool> _showFps; private static MelonPreferences_Entry<bool> _moreNpcsCompat; internal static bool EnableLod => _enableLod?.Value ?? true; internal static bool EnableInMultiplayer => _enableInMp?.Value ?? true; internal static float CosmeticDistance => Mathf.Clamp(_cosmeticDistance?.Value ?? 40f, 15f, 300f); internal static float DeepDistance => Mathf.Clamp(_deepDistance?.Value ?? 80f, 30f, 500f); internal static float Hysteresis => Mathf.Clamp(_hysteresis?.Value ?? 8f, 0f, 50f); internal static int BudgetPerFrame => Mathf.Clamp(_budgetPerFrame?.Value ?? 32, 4, 256); internal static bool UseCosmeticCull => _useCosmeticCull?.Value ?? true; internal static bool UseDeepCull => _useDeepCull?.Value ?? true; internal static bool RespectOnScreen => _respectOnScreen?.Value ?? true; internal static bool ShowFpsCounter => _showFps?.Value ?? false; internal static bool MoreNpcsCompat => _moreNpcsCompat?.Value ?? true; internal static void Initialize() { if (_category == null) { _category = MelonPreferences.CreateCategory("Siesta_01_Main", "Siesta (NPC Performance)"); _enableLod = Create("EnableLod", def: true, "Enable NPC LOD", "Master switch. When ON (default), NPCs that are off-screen and far from you are culled to recover performance, and restored as you approach. Turn OFF to run fully vanilla (everything restored)."); _enableInMp = Create("EnableInMultiplayer", def: true, "Enable in multiplayer", "ON (default): in a co-op session, cosmetic culling still runs locally (safe - it only hides distant NPCs on YOUR screen), and the deeper movement/schedule culling runs ONLY on the host (who owns NPC simulation). OFF: the mod does nothing in multiplayer."); _cosmeticDistance = Create("CosmeticDistance", 40f, "Cosmetic cull distance (m)", "Off-screen NPCs farther than this are hidden (renderer off) to save animation/draw cost. They keep moving and behaving normally. Lower = more FPS / nearer horizon. Clamped 15-300.", (ValueValidator)(object)new ValueRange<float>(15f, 300f)); _deepDistance = Create("DeepDistance", 80f, "Deep cull distance (m)", "Off-screen, idle, non-essential NPCs farther than this also have their movement + schedule paused (the biggest saving). They catch up to their correct schedule position when you return. Dealers/employees/customers mid-task, story NPCs, and anyone near any player are never deep-culled. Host-only in multiplayer. Clamped 30-500.", (ValueValidator)(object)new ValueRange<float>(30f, 500f)); _hysteresis = Create("Hysteresis", 8f, "Hysteresis margin (m)", "Dead-zone around each distance boundary so NPCs don't flicker between states when you stand near an edge. Clamped 0-50.", (ValueValidator)(object)new ValueRange<float>(0f, 50f)); _budgetPerFrame = Create("BudgetPerFrame", 32, "NPCs re-evaluated per frame", "The mod re-checks this many NPCs each frame on a rolling cursor (so the whole population is covered every few frames) - keeps the mod's own cost flat. Lower = cheaper but slower to react. Clamped 4-256.", (ValueValidator)(object)new ValueRange<int>(4, 256)); _useCosmeticCull = Create("UseCosmeticCull", def: true, "Use cosmetic culling (hide distant NPCs)", "ON (default): hide off-screen distant NPCs (renderer off). Purely cosmetic and always safe. OFF: never hide NPCs (disables the cheapest, safest tier)."); _useDeepCull = Create("UseDeepCull", def: true, "Use deep culling (pause movement + schedule)", "ON (default): also pause movement + schedule for far, idle, non-essential NPCs (recovers the biggest cost, the NavMeshAgent). Fully reversible with schedule catch-up. OFF: cosmetic culling only (maximally conservative - zero AI impact, smaller FPS gain)."); _respectOnScreen = Create("RespectOnScreen", def: true, "Never cull on-screen NPCs", "ON (default): an NPC roughly in front of the camera is never culled, even when far, so you never see a visible NPC freeze or pop. Recommended ON."); _showFps = Create("ShowFpsCounter", def: false, "Show FPS counter", "Small on-screen FPS readout (top-right). OFF by default. Applies live."); _moreNpcsCompat = Create("MoreNpcsAutoCompat", def: true, "MoreNPCs auto-compat", "ON (default). Siesta auto-detects \"Fannso's MoreNPCs\" and, ONLY if its build is incompatible with this IL2CPP install (it would throw a TypeLoadException every frame), neutralizes its crashing per-frame watcher (MoreNPCs.Core.OnUpdate) so the game stays stable - MoreNPCs' NPCs still spawn via S1API. No effect if MoreNPCs is absent or already compatible. Turn OFF to never apply it. Requires a game restart to take effect."); } } private static MelonPreferences_Entry<T> Create<T>(string id, T def, string name, string desc = null, ValueValidator validator = null) { if (validator != null) { return _category.CreateEntry<T>(id, def, name, desc, false, false, validator); } return _category.CreateEntry<T>(id, def, name, desc, false, false, (ValueValidator)null, (string)null); } } } namespace Siesta.Compat { internal static class MoreNpcsCompat { private const string ProbeType = "ScheduleOne.UI.Handover.HandoverScreen"; internal static void Apply(Harmony harmony) { //IL_00b1: Unknown result type (might be due to invalid IL or missing references) //IL_00b7: Expected O, but got Unknown try { Assembly assembly = FindAssembly("MoreNPCs"); if (assembly == null) { return; } Assembly assembly2 = FindAssembly("Assembly-CSharp"); if (assembly2 != null && assembly2.GetType("ScheduleOne.UI.Handover.HandoverScreen", throwOnError: false) != null) { Instance log = Core.Log; if (log != null) { log.Msg("[Siesta] MoreNPCs detected and compatible with this install - compat shim not needed."); } return; } MethodInfo methodInfo = assembly.GetType("MoreNPCs.Core", throwOnError: false)?.GetMethod("OnUpdate", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (methodInfo == null) { Instance log2 = Core.Log; if (log2 != null) { log2.Warning("[Siesta] MoreNPCs compat: MoreNPCs.Core.OnUpdate not found - skipped."); } return; } HarmonyMethod val = new HarmonyMethod(typeof(MoreNpcsCompat).GetMethod("SkipOriginal", BindingFlags.Static | BindingFlags.NonPublic)); harmony.Patch((MethodBase)methodInfo, val, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); Instance log3 = Core.Log; if (log3 != null) { log3.Msg("[Siesta] MoreNPCs (incompatible build for this IL2CPP install) auto-detected - compat shim applied: skipping MoreNPCs.Core.OnUpdate to stop the per-frame TypeLoadException storm; its NPCs still spawn via S1API."); } } catch (Exception ex) { Instance log4 = Core.Log; if (log4 != null) { log4.Warning("[Siesta] MoreNPCs compat shim failed: " + ex.Message); } } } private static Assembly FindAssembly(string simpleName) { Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); for (int i = 0; i < assemblies.Length; i++) { try { if (assemblies[i].GetName().Name == simpleName) { return assemblies[i]; } } catch { } } return null; } private static bool SkipOriginal() { return false; } } internal static class Net { internal static bool IsMultiplayer() { try { Lobby instance = Singleton<Lobby>.Instance; if ((Object)(object)instance == (Object)null) { return false; } return instance.IsInLobby && instance.PlayerCount > 1; } catch { return false; } } internal static bool IsAuthoritative() { try { Lobby instance = Singleton<Lobby>.Instance; if ((Object)(object)instance == (Object)null || !instance.IsInLobby) { return true; } return instance.IsHost; } catch { return true; } } } }