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.2.2
Siesta.dll
Decompiled 8 hours agousing System; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; using System.Reflection.Emit; using System.Resources; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using HarmonyLib; using Il2CppFishNet.Object; using Il2CppInterop.Runtime.InteropTypes; using Il2CppScheduleOne.DevUtilities; using Il2CppScheduleOne.Economy; using Il2CppScheduleOne.Employees; using Il2CppScheduleOne.GameTime; using Il2CppScheduleOne.NPCs; using Il2CppScheduleOne.NPCs.Behaviour; using Il2CppScheduleOne.NPCs.Schedules; using Il2CppScheduleOne.Networking; using Il2CppScheduleOne.PlayerScripts; using Il2CppScheduleOne.Police; using Il2CppSystem; 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.2.2", "DooDesch", "https://github.com/DooDesch-Mods/ScheduleOne-Siesta")] [assembly: MelonGame("TVGS", "Schedule I")] [assembly: TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName = ".NET 6.0")] [assembly: AssemblyCompany("DooDesch")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyCopyright("Copyright © DooDesch")] [assembly: AssemblyFileVersion("1.2.2.0")] [assembly: AssemblyInformationalVersion("1.2.2+94fd76f3d8adb5fbaa3807bc55da353536a7a615")] [assembly: AssemblyProduct("Siesta")] [assembly: AssemblyTitle("Siesta")] [assembly: NeutralResourcesLanguage("en-US")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.2.2.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; private Action _onDayPass; private bool _dayPassHooked; 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(); MoreNpcsCompat.Apply(((MelonBase)this).HarmonyInstance); GameLifecycle.OnSaveStart += delegate { LodController.RestoreAll("save starting"); }; GameLifecycle.OnPreSceneChange += delegate { LodController.RestoreAll("scene changing"); }; Log.Msg("Siesta v1.2.2 - NPC LOD active."); } public override void OnSceneWasLoaded(int buildIndex, string sceneName) { _inWorld = sceneName == "Main"; if (_inWorld) { HookDayPass(); } } public override void OnSceneWasUnloaded(int buildIndex, string sceneName) { _inWorld = false; UnhookDayPass(); LodController.Reset(); } public override void OnUpdate() { if (_inWorld) { if (!_dayPassHooked) { HookDayPass(); } LodController.Tick(); TelemetryTick(); } } public override void OnGUI() { if (_inWorld && Preferences.ShowFpsCounter) { FpsCounter.Draw(); } } public override void OnApplicationQuit() { UnhookDayPass(); LodController.RestoreAll("application quit"); } public override void OnDeinitializeMelon() { UnhookDayPass(); LodController.RestoreAll("melon unload"); } private void HookDayPass() { if (_dayPassHooked) { return; } try { if (!NetworkSingleton<TimeManager>.InstanceExists) { return; } TimeManager instance = NetworkSingleton<TimeManager>.Instance; if (!((Object)(object)instance == (Object)null)) { _onDayPass = Action.op_Implicit((Action)delegate { LodController.RestoreAll("new day"); }); instance.onDayPass += _onDayPass; _dayPassHooked = true; } } catch (Exception ex) { Log.Warning("[Siesta] onDayPass hook failed: " + ex.Message); } } private void UnhookDayPass() { if (!_dayPassHooked) { return; } try { if (NetworkSingleton<TimeManager>.InstanceExists) { TimeManager instance = NetworkSingleton<TimeManager>.Instance; if ((Object)(object)instance != (Object)null && (Delegate)(object)_onDayPass != (Delegate)null) { instance.onDayPass -= _onDayPass; } } } catch { } finally { _onDayPass = null; _dayPassHooked = false; } } 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 ((Object)(object)st.Officer != (Object)null) { return "police"; } if ((Object)(object)npc.DialogueHandler != (Object)null && npc.DialogueHandler.IsDialogueInProgress) { return "in-dialogue"; } if (npc.isInBuilding) { return "in-building"; } NPCScheduleManager schedule = st.Schedule; if ((Object)(object)schedule != (Object)null) { NPCAction activeAction = schedule.ActiveAction; if ((Object)(object)activeAction != (Object)null && (Object)(object)((Il2CppObjectBase)activeAction).TryCast<NPCEvent_StayInBuilding>() != (Object)null) { return "building-transit"; } } NPCBehaviour behaviour = npc.Behaviour; if ((Object)(object)behaviour != (Object)null && (Object)(object)behaviour.activeBehaviour != (Object)null && (((Object)(object)behaviour.CombatBehaviour != (Object)null && ((Behaviour)behaviour.CombatBehaviour).Active) || ((Object)(object)behaviour.FleeBehaviour != (Object)null && ((Behaviour)behaviour.FleeBehaviour).Active) || ((Object)(object)behaviour.CoweringBehaviour != (Object)null && ((Behaviour)behaviour.CoweringBehaviour).Active) || ((Object)(object)behaviour.CallPoliceBehaviour != (Object)null && ((Behaviour)behaviour.CallPoliceBehaviour).Active) || ((Object)(object)behaviour.RequestProductBehaviour != (Object)null && ((Behaviour)behaviour.RequestProductBehaviour).Active) || ((Object)(object)behaviour.GenericDialogueBehaviour != (Object)null && ((Behaviour)behaviour.GenericDialogueBehaviour).Active))) { return "active-critical-behaviour"; } if (ExemptOnAnyBehaviour && (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 { private struct FailInfo { public float RetryAt; public int Count; } internal enum Control { Auto, ForceFull, ForceCosmetic, ForceDeep } private static int _cursor; private const float RetryAfterSeconds = 60f; private const int MaxConsecutiveFailures = 3; private static readonly Dictionary<int, FailInfo> _failed = new Dictionary<int, FailInfo>(); 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; private static bool IsSpawned(NPC npc) { try { NetworkObject networkObject = ((NetworkBehaviour)npc).NetworkObject; return (Object)(object)networkObject != (Object)null && networkObject.IsSpawned; } catch { return false; } } private static bool IsFailHeld(int id) { if (!_failed.TryGetValue(id, out var value)) { return false; } if (value.Count >= 3) { return true; } return Time.unscaledTime < value.RetryAt; } private static void NoteFailure(int id) { _failed.TryGetValue(id, out var value); value.Count++; value.RetryAt = Time.unscaledTime + 60f; _failed[id] = value; } internal static void Tick() { //IL_0109: Unknown result type (might be due to invalid IL or missing references) //IL_010e: Unknown result type (might be due to invalid IL or missing references) //IL_0115: Unknown result type (might be due to invalid IL or missing references) //IL_0123: Unknown result type (might be due to invalid IL or missing references) 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(); float num = Preferences.CosmeticDistance * Preferences.CosmeticDistance; bool respectOnScreen = Preferences.RespectOnScreen; for (int i = 0; i < count; i++) { NPC val; try { val = nPCRegistry[i]; } catch { continue; } if ((Object)(object)val == (Object)null || !IsSpawned(val)) { continue; } int instanceID; try { instanceID = ((Object)val).GetInstanceID(); } catch { continue; } if (IsFailHeld(instanceID)) { continue; } NpcModState orAdd = LodRegistry.GetOrAdd(instanceID, val); if (orAdd.Tier == LodState.Full) { continue; } Vector3 centerPoint; try { centerPoint = val.CenterPoint; } catch { continue; } if (!(MinSqrDistToPlayer(centerPoint) < num) && (!respectOnScreen || !IsOnScreen(centerPoint))) { continue; } try { LodLevers.ApplyTier(val, orAdd, LodState.Full, authoritative); } catch (Exception ex) { Instance log = Core.Log; if (log != null) { log.Warning("[Siesta] promote pre-pass failed: " + ex.Message); } } } int num2 = Math.Min(Preferences.BudgetPerFrame, count); for (int j = 0; j < num2; j++) { if (_cursor >= count) { _cursor = 0; } NPC val2; try { val2 = nPCRegistry[_cursor]; } catch { _cursor++; continue; } _cursor++; if (!((Object)(object)val2 == (Object)null) && IsSpawned(val2)) { Evaluate(val2, authoritative); } } } private static void Evaluate(NPC npc, bool authoritative) { //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_0059: 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_0069: 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 (IsFailHeld(instanceID)) { if (orAdd.Tier != LodState.Full) { LodLevers.ForceFull(npc, orAdd); } return; } orAdd.Resolve(npc); if (orAdd.Hidden) { try { if (npc.isVisible) { orAdd.Hidden = false; } } catch { } } 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; NoteFailure(instanceID); Instance log = Core.Log; if (log != null) { log.Warning($"[Siesta] NPC {instanceID} wake failed - keeping Full, will retry in {60f:F0}s."); } 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 || !IsSpawned(val)) { continue; } int instanceID; try { instanceID = ((Object)val).GetInstanceID(); } catch { continue; } if (!IsFailHeld(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, authoritative); } break; case LodState.Deep: if (!st.DeepApplied) { ApplyDeep(npc, st, authoritative); } if (!st.Hidden) { Hide(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, bool authoritative) { npc.SetVisible(false, false); if (authoritative) { try { NPCMovement movement = npc.Movement; if ((Object)(object)movement != (Object)null) { movement.SetAgentEnabled(true); movement.VisibilityChange(true); } } catch { } } st.Hidden = true; } private static void Show(NPC npc, NpcModState st) { bool flag = false; try { flag = npc.isInBuilding || npc.IsInVehicle; } catch { } if (!flag) { 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) { try { NavMeshAgent agent = movement.Agent; if ((Object)(object)agent != (Object)null && ((Behaviour)agent).enabled && agent.isOnNavMesh) { movement.PauseMovement(); st.WePausedMovement = true; } } catch { } } NPCScheduleManager schedule = st.Schedule; if ((Object)(object)schedule != (Object)null && schedule.ScheduleEnabled) { schedule.DisableSchedule(); st.WeDisabledSchedule = true; } try { NPCAwareness awareness = npc.Awareness; if ((Object)(object)awareness != (Object)null) { awareness.SetAwarenessActive(false); st.WeDisabledAwareness = true; } } catch { } st.DeepApplied = true; } private static void RestoreDeep(NPC npc, NpcModState st) { if (st.WeDisabledAwareness) { try { NPCAwareness awareness = npc.Awareness; if ((Object)(object)awareness != (Object)null) { awareness.SetAwarenessActive(true); } } catch { } st.WeDisabledAwareness = 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; return; } } NPCScheduleManager schedule = st.Schedule; if (st.WeDisabledSchedule && (Object)(object)schedule != (Object)null) { schedule.EnableSchedule(); schedule.EnforceState(false); st.WeDisabledSchedule = false; } st.DeepApplied = false; } private static bool RepairNavMesh(NPC npc, NPCMovement mv, NpcModState st) { //IL_007c: Unknown result type (might be due to invalid IL or missing references) //IL_00c1: Unknown result type (might be due to invalid IL or missing references) //IL_0098: 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; } NavMeshHit val = default(NavMeshHit); if (mv.SmartSampleNavMesh(mv.FootPosition, ref val, 1f, 10f, 3)) { mv.Warp(((NavMeshHit)(ref val)).position); mv.SetAgentEnabled(false); mv.SetAgentEnabled(true); 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 WeDisabledAwareness; internal bool WakeFailed; internal string ExemptReason; private bool _resolved; internal NPCScheduleManager Schedule; internal Dealer Dealer; internal Employee Employee; internal Customer Customer; internal PoliceOfficer Officer; 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 { } try { Officer = ((Il2CppObjectBase)npc).TryCast<PoliceOfficer>(); } 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; 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 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."); } } 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 WatcherType = "PPHylandHandoverWarning"; private const string WatcherMethod = "RefreshThrottled"; private static MethodInfo _watcher; private static Action _invoke; private static bool _givenUp; internal static void Apply(Harmony harmony) { //IL_0073: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Expected O, but got Unknown try { Assembly assembly = FindAssembly("MoreNPCs"); if (assembly == null) { return; } MethodInfo methodInfo = assembly.GetType("MoreNPCs.Core", throwOnError: false)?.GetMethod("OnUpdate", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (methodInfo == null) { Instance log = Core.Log; if (log != null) { log.Warning("[Siesta] MoreNPCs compat: MoreNPCs.Core.OnUpdate not found - skipped."); } } else { harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(typeof(MoreNpcsCompat).GetMethod("Transpile", BindingFlags.Static | BindingFlags.NonPublic)), (HarmonyMethod)null, (HarmonyMethod)null); } } catch (Exception ex) { Instance log2 = Core.Log; if (log2 != null) { log2.Warning("[Siesta] MoreNPCs compat failed: " + ex.Message); } } } private static IEnumerable<CodeInstruction> Transpile(IEnumerable<CodeInstruction> instructions) { List<CodeInstruction> list = new List<CodeInstruction>(instructions); MethodInfo method = typeof(MoreNpcsCompat).GetMethod("Guarded", BindingFlags.Static | BindingFlags.NonPublic); for (int i = 0; i < list.Count; i++) { CodeInstruction val = list[i]; if ((!(val.opcode != OpCodes.Call) || !(val.opcode != OpCodes.Callvirt)) && val.operand is MethodInfo methodInfo && !(methodInfo.Name != "RefreshThrottled") && !(methodInfo.DeclaringType == null) && !(methodInfo.DeclaringType.Name != "PPHylandHandoverWarning") && methodInfo.GetParameters().Length == 0 && methodInfo.IsStatic) { _watcher = methodInfo; val.opcode = OpCodes.Call; val.operand = method; return list; } } Instance log = Core.Log; if (log != null) { log.Msg("[Siesta] MoreNPCs compat: no PPHylandHandoverWarning.RefreshThrottled call in OnUpdate - nothing to guard, MoreNPCs left untouched."); } return list; } private static void Guarded() { if (_givenUp) { return; } try { if (_invoke == null) { _invoke = (Action)Delegate.CreateDelegate(typeof(Action), _watcher); } _invoke(); } catch (Exception ex) { _givenUp = true; Instance log = Core.Log; if (log != null) { log.Warning("[Siesta] MoreNPCs' PPHylandHandoverWarning cannot run on this install (" + ex.GetType().Name + ") - Siesta will not call it again this session. This usually means the CrossCompat/Mono MoreNPCs build is installed on a standard IL2CPP game; the rest of MoreNPCs is unaffected."); } } } 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; } } 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; } } } }