using 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 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 _failed = new HashSet(); 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 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 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 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 _states = new Dictionary(); 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 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 dictionary = new Dictionary(); foreach (KeyValuePair 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 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.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(true); } catch { } try { Dealer = ((Il2CppObjectBase)npc).TryCast(); } catch { } try { Employee = ((Il2CppObjectBase)npc).TryCast(); } catch { } try { Customer = ((Component)npc).GetComponent(); } catch { } } } } namespace Siesta.Config { internal static class Preferences { private const string CategoryId = "Siesta_01_Main"; private static MelonPreferences_Category _category; private static MelonPreferences_Entry _enableLod; private static MelonPreferences_Entry _enableInMp; private static MelonPreferences_Entry _cosmeticDistance; private static MelonPreferences_Entry _deepDistance; private static MelonPreferences_Entry _hysteresis; private static MelonPreferences_Entry _budgetPerFrame; private static MelonPreferences_Entry _useCosmeticCull; private static MelonPreferences_Entry _useDeepCull; private static MelonPreferences_Entry _respectOnScreen; private static MelonPreferences_Entry _showFps; private static MelonPreferences_Entry _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(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(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(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(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 Create(string id, T def, string name, string desc = null, ValueValidator validator = null) { if (validator != null) { return _category.CreateEntry(id, def, name, desc, false, false, validator); } return _category.CreateEntry(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.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.Instance; if ((Object)(object)instance == (Object)null || !instance.IsInLobby) { return true; } return instance.IsHost; } catch { return true; } } } }