using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; 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 System.Threading.Tasks; using FishNet; using FishNet.Managing; using FishNet.Object; using HarmonyLib; using K9_Patrol.Source; using K9_Patrol.Source.Controllers; using K9_Patrol.Source.Managers; using K9_Patrol.Source.NPCs; using K9_Patrol.Source.Networking; using K9_Patrol.Source.Utils; using MelonLoader; using MelonLoader.Preferences; using MelonLoader.Utils; using Microsoft.CodeAnalysis; using ScheduleOne; using ScheduleOne.Combat; using ScheduleOne.Core.Items.Framework; using ScheduleOne.DevUtilities; using ScheduleOne.ItemFramework; using ScheduleOne.Law; using ScheduleOne.Map; using ScheduleOne.NPCs; using ScheduleOne.NPCs.Behaviour; using ScheduleOne.NPCs.Responses; using ScheduleOne.PlayerScripts; using ScheduleOne.Police; using ScheduleOne.Product; using ScheduleOne.Product.Packaging; using ScheduleOne.Storage; using ScheduleOne.Vision; using SteamNetworkLib; using SteamNetworkLib.Core; using SteamNetworkLib.Events; using SteamNetworkLib.Models; using SteamNetworkLib.Utilities; using Steamworks; using UnityEngine; using UnityEngine.AI; using UnityEngine.Events; using UnityEngine.Networking; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: MelonInfo(typeof(Core), "K9 Patrol", "1.1.0", "DropDaDeuce", null)] [assembly: MelonGame("TVGS", "Schedule I")] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: IgnoresAccessChecksTo("Assembly-CSharp")] [assembly: AssemblyCompany("K9_Patrol_Mono")] [assembly: AssemblyConfiguration("MONO")] [assembly: AssemblyFileVersion("1.1.0.0")] [assembly: AssemblyInformationalVersion("1.1.0+0086fe93cf972ddc8e38e9eb58156e1642441df3")] [assembly: AssemblyProduct("K9_Patrol_Mono")] [assembly: AssemblyTitle("K9_Patrol_Mono")] [assembly: NeutralResourcesLanguage("en-US")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.1.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 K9_Patrol { internal static class BuildVersion { internal const string Value = "1.1.0"; } } namespace K9_Patrol.Source { public sealed class Core : MelonMod { public static string cfgDir; internal static AssetBundle bundle; private static bool _pumpFaulted; public override void OnInitializeMelon() { K9Config.Init(); LoadAssets(); K9DogHit.Install(((MelonBase)this).HarmonyInstance); K9Audio.Preload(); try { K9NetSync.InitializeIfNeeded(); } catch (Exception ex) { Logger.Error("Could not initialise K9 networking: " + ex.Message + ". Single-player is unaffected."); } Logger.Msg("K9 Patrol mod initialized."); } public override void OnUpdate() { try { K9NetSync.Pump(); K9NetSync.PostPump(); } catch (Exception arg) { if (!_pumpFaulted) { _pumpFaulted = true; Logger.Error($"K9 pump threw and has been reported once: {arg}"); } } } public override void OnDeinitializeMelon() { try { K9SteamLink.Shutdown(); } catch { } } public override void OnSceneWasInitialized(int buildIndex, string sceneName) { //IL_006a: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Expected O, but got Unknown Logger.Debug($"Scene initialized: {sceneName} (Build Index: {buildIndex})"); try { K9NetSync.ServerClearAll(); K9NetSync.ClientClearAll(); K9ContrabandRegistry.Clear(); K9ContrabandPublisher.Reset(); K9ScentTable.Rearm(); } catch (Exception ex) { Logger.Debug("K9 scene reset: " + ex.Message); } if ((Object)(object)Object.FindObjectOfType() == (Object)null) { GameObject val = new GameObject("K9UnitManager"); Object.DontDestroyOnLoad((Object)(object)val); val.AddComponent(); Logger.Debug("K9Manager created and added to the scene."); } } private static void LoadAssets() { try { bundle = AssetBundleUtils.LoadAssetBundle("K9_Patrol.Assets.k9model.bundle"); if ((Object)(object)bundle != (Object)null) { Logger.Msg("AssetBundle loaded successfully. Contents:"); string[] allAssetNames = bundle.GetAllAssetNames(); if (allAssetNames != null && allAssetNames.Length != 0) { string[] array = allAssetNames; foreach (string text in array) { Logger.Msg(" - " + text); } } else { Logger.Warning("Bundle appears to be empty."); } } else { Logger.Error("Failed to load AssetBundle - returned null"); } } catch (Exception arg) { Logger.Error($"Failed to load AssetBundle: {arg}"); } } } internal static class K9Config { internal static MelonPreferences_Category Cat; internal static MelonPreferences_Entry UnitCount; internal static MelonPreferences_Entry SniffRadius; internal static MelonPreferences_Entry ScentFullRadiusScore; internal static MelonPreferences_Entry ScentMaxRadiusMultiplier; internal static MelonPreferences_Entry RecheckInterval; internal static MelonPreferences_Entry BodySearchStartDistance; internal static MelonPreferences_Entry SearchCooldown; internal static MelonPreferences_Entry PursuitSpeedMultiplier; internal static MelonPreferences_Entry DogBaseSpeed; internal static MelonPreferences_Entry DogAcceleration; internal static MelonPreferences_Entry DogAngularSpeed; internal static MelonPreferences_Entry CatchupStartDistance; internal static MelonPreferences_Entry CatchupMaxMultiplier; internal static MelonPreferences_Entry HardWarpDistance; internal static MelonPreferences_Entry HardWarpVerticalDelta; internal static MelonPreferences_Entry WarpCooldown; internal static MelonPreferences_Entry DogAnimationSpeedScale; internal static MelonPreferences_Entry AmbientBarkEnabled; internal static MelonPreferences_Entry AmbientBarkRadius; internal static MelonPreferences_Entry AmbientBarkMinInterval; internal static MelonPreferences_Entry AmbientBarkMaxInterval; internal static MelonPreferences_Entry DebugLogging; private const string HostNote = "MULTIPLAYER: the host decides this one for everybody. Your copy is only used when you are hosting."; private const string YoursNote = "MULTIPLAYER: this one is always yours, host or not."; public static int MaxUnitCount => UnitCount.Value; internal static void Init() { string text = Path.Combine(MelonEnvironment.UserDataDirectory, "DropDaDeuce-K9Patrol"); string text2 = Path.Combine(text, "K9Patrol.cfg"); Directory.CreateDirectory(text); Cat = MelonPreferences.CreateCategory("K9Unit"); Cat.SetFilePath(text2, File.Exists(text2), true); UnitCount = Cat.CreateEntry("UnitCount", 2, "Maximum K9 Units", "How many officer-and-dog pairs are out on patrol at once. They are put onto the game's own police foot-patrol routes, so more of them means you run into one more often, not that they hunt you harder. MULTIPLAYER: the host decides this one for everybody. Your copy is only used when you are hosting.", false, false, (ValueValidator)null, (string)null); SniffRadius = Cat.CreateEntry("SniffRadius", 10f, "Sniff Radius (m)", "How far a dog can smell contraband, in metres, as a starting point rather than a fixed distance. What you are carrying stretches or shrinks it: see Scent Score For Full Sniff Radius below, and K9Scents.cfg for the per-drug and per-packaging numbers. A wall, floor or door between you and the dog cuts whatever is left to about a third. In multiplayer the host's K9Scents.cfg is used for everybody, the same as this setting - and that file is also where the backpack number lives, under [containers]. MULTIPLAYER: the host decides this one for everybody. Your copy is only used when you are hosting.", false, false, (ValueValidator)null, (string)null); ScentFullRadiusScore = Cat.CreateEntry("ScentFullRadiusScore", 20f, "Scent Score For Full Sniff Radius", "How much contraband counts as \"a normal amount\". Carry that much and the dog smells you at the full Sniff Radius; carry less and it has to get closer; carry more and it reaches further, up to the cap below. The scores come from K9Scents.cfg, where one unit of any loose drug is worth 1. At 20, a full stack of loose product is smelled at the full radius, ten of it at about half that, and it takes thirty to reach the cap. LOWER THIS NUMBER to make the dogs sharper; raise it to let yourself carry more before you are noticed. MULTIPLAYER: the host decides this one for everybody. Your copy is only used when you are hosting.", false, false, (ValueValidator)null, (string)null); ScentMaxRadiusMultiplier = Cat.CreateEntry("ScentMaxRadiusMultiplier", 1.5f, "Maximum Sniff Radius Multiplier", "A hard limit on how far a huge stash can push the sniff radius, written as a multiple of Sniff Radius. At 1.5 with the default 10 m radius, no amount of contraband will ever let a dog smell you from beyond 15 m. MULTIPLAYER: the host decides this one for everybody. Your copy is only used when you are hosting.", false, false, (ValueValidator)null, (string)null); RecheckInterval = Cat.CreateEntry("RecheckInterval", 0.2f, "Detection Poll Interval (s)", "How often, in seconds, each dog checks whether anyone nearby is carrying. Lower makes them react faster and costs a little more performance; there is no reason to go below 0.1 or above about 1. MULTIPLAYER: the host decides this one for everybody. Your copy is only used when you are hosting.", false, false, (ValueValidator)null, (string)null); BodySearchStartDistance = Cat.CreateEntry("BodySearchStartDistance", 1.25f, "Body Search Start Distance (m)", "Metres. How close the officer has to actually get before the body search begins. That is the game's own search, with all the consequences that normally come with it. Raise it and you get searched from further away; lower it and the officer has to walk right up to you. MULTIPLAYER: the host decides this one for everybody. Your copy is only used when you are hosting.", false, false, (ValueValidator)null, (string)null); SearchCooldown = Cat.CreateEntry("SearchCooldown", 30f, "Search Cooldown (s)", "Seconds of peace after a search before any K9 will start on the same person again. It is shared between all the dogs, so being cleared by one officer does not hand you straight to the next. MULTIPLAYER: the host decides this one for everybody. Your copy is only used when you are hosting.", false, false, (ValueValidator)null, (string)null); PursuitSpeedMultiplier = Cat.CreateEntry("PursuitSpeedMultiplier", 1.25f, "Officer Pursuit Speed Multiplier", "How much faster the officer moves while chasing you, compared to their normal patrol speed. 1 is no faster at all; 1.25 is a quarter faster. The boost is given back the moment the chase ends. MULTIPLAYER: the host decides this one for everybody. Your copy is only used when you are hosting.", false, false, (ValueValidator)null, (string)null); DogBaseSpeed = Cat.CreateEntry("DogBaseSpeed", 3f, "Dog Base Speed", "The dog's ordinary speed in metres per second, while it is trotting along beside its handler. It goes faster than this only when it has fallen behind. See the two catch-up settings below. MULTIPLAYER: the host decides this one for everybody. Your copy is only used when you are hosting.", false, false, (ValueValidator)null, (string)null); DogAcceleration = Cat.CreateEntry("DogAcceleration", 14f, "Dog Acceleration", "How quickly the dog gets up to speed and slows down again. High numbers look eager and a bit twitchy; low numbers make it drift into a stop like it is on ice. MULTIPLAYER: the host decides this one for everybody. Your copy is only used when you are hosting.", false, false, (ValueValidator)null, (string)null); DogAngularSpeed = Cat.CreateEntry("DogAngularSpeed", 720f, "Dog Angular Speed", "How fast the dog can swing round WHILE IT IS MOVING, in degrees per second, so how tightly it corners. 720 is two full turns a second, brisk enough that it never looks like it is steering a bus. How fast it spins on the spot to face someone is fixed, and is not this setting. MULTIPLAYER: the host decides this one for everybody. Your copy is only used when you are hosting.", false, false, (ValueValidator)null, (string)null); CatchupStartDistance = Cat.CreateEntry("CatchupStartDistance", 6f, "Catch-up Start Distance (m)", "Metres. How far behind its handler the dog is allowed to drift before it breaks into a run to catch up. MULTIPLAYER: the host decides this one for everybody. Your copy is only used when you are hosting.", false, false, (ValueValidator)null, (string)null); CatchupMaxMultiplier = Cat.CreateEntry("CatchupMaxMultiplier", 2.2f, "Catch-up Max Speed Multiplier", "The fastest the dog is allowed to go while catching up, as a multiple of Dog Base Speed. 2.2 means it can sprint at a little over twice its walking pace. It eases back down as it closes the gap rather than stopping dead. MULTIPLAYER: the host decides this one for everybody. Your copy is only used when you are hosting.", false, false, (ValueValidator)null, (string)null); HardWarpDistance = Cat.CreateEntry("HardWarpDistance", 30f, "Hard Warp Distance (m)", "Metres. A dog that ends up further than this from its handler gives up walking and teleports back to them. Stairs, doorways and lifts strand dogs, and this is what rescues them. Seeing dogs blink around? Raise it. Seeing dogs stuck far behind? Lower it. MULTIPLAYER: the host decides this one for everybody. Your copy is only used when you are hosting.", false, false, (ValueValidator)null, (string)null); HardWarpVerticalDelta = Cat.CreateEntry("HardWarpVerticalDelta", 5f, "Hard Warp Vertical Delta (m)", "Metres of height difference that also triggers the teleport above, even when the dog is not far away on the map. This is the one that fetches a dog left on the floor below when its handler goes upstairs. MULTIPLAYER: the host decides this one for everybody. Your copy is only used when you are hosting.", false, false, (ValueValidator)null, (string)null); WarpCooldown = Cat.CreateEntry("WarpCooldown", 4f, "Warp Cooldown (s)", "The shortest gap allowed between two teleports, so a dog that cannot find its way cannot strobe in and out of existence. MULTIPLAYER: the host decides this one for everybody. Your copy is only used when you are hosting.", false, false, (ValueValidator)null, (string)null); DogAnimationSpeedScale = Cat.CreateEntry("DogAnimationSpeedScale", 4.5f, "Dog Animation Speed", "How fast the dog's legs move, as a multiplier, when it is running at full speed. Raise it if the dog looks like it is skating; lower it if its legs look like they are spinning. It scales with how fast the dog is actually going, so changing Dog Base Speed does not require changing this too. MULTIPLAYER: this one is always yours, host or not.", false, false, (ValueValidator)null, (string)null); AmbientBarkEnabled = Cat.CreateEntry("AmbientBarkEnabled", true, "Dogs Bark While Idle", "true (default): a K9 woofs to itself now and then while patrolling, as long as somebody is close enough to hear it. That is just ambience. The urgent bark it makes when it has actually smelled something on you is a different sound and is not affected by this. Set false for silent dogs. MULTIPLAYER: this one is always yours, host or not.", false, false, (ValueValidator)null, (string)null); AmbientBarkRadius = Cat.CreateEntry("AmbientBarkRadius", 15f, "Idle Bark Audible Radius (m)", "Metres. A patrolling dog only bothers woofing if somebody is within this far of it, so the map is not full of barking you cannot hear. MULTIPLAYER: this one is always yours, host or not.", false, false, (ValueValidator)null, (string)null); AmbientBarkMinInterval = Cat.CreateEntry("AmbientBarkMinInterval", 14f, "Idle Bark Minimum Gap (s)", "Seconds. The shortest gap there can be between two woofs from the same dog. MULTIPLAYER: this one is always yours, host or not.", false, false, (ValueValidator)null, (string)null); AmbientBarkMaxInterval = Cat.CreateEntry("AmbientBarkMaxInterval", 45f, "Idle Bark Maximum Gap (s)", "Seconds. The longest gap there can be between two woofs. Each dog rolls a new gap at random between the minimum and this one, so they do not all bark in time with each other. MULTIPLAYER: this one is always yours, host or not.", false, false, (ValueValidator)null, (string)null); DebugLogging = Cat.CreateEntry("DebugLogging", false, "Enable Debug Logging", "Writes a lot of extra detail to MelonLoader\\Latest.log. TURN THIS ON BEFORE REPORTING A PROBLEM and attach the log: how the dog worked out what it could smell, and how it decided where to walk, are both completely silent without it. If the problem is in a multiplayer game, turn it on for everyone in the lobby and send every log. One machine on its own almost never shows what went wrong. MULTIPLAYER: this one is always yours, host or not.", false, false, (ValueValidator)null, (string)null); MelonPreferences.Save(); } } public static class Logger { internal static void Debug(string msg) { if (K9Config.DebugLogging.Value) { MelonLogger.Msg("[K9 Debug] " + msg); } } internal static void Warning(string msg) { MelonLogger.Warning("[K9] " + msg); } internal static void Error(string msg) { MelonLogger.Error("[K9] " + msg); } internal static void Msg(string msg) { MelonLogger.Msg("[K9] " + msg); } } } namespace K9_Patrol.Source.Managers { public sealed class K9Manager : MonoBehaviour { private static readonly List _units = new List(); private float _checkTimer = 0f; private const float CHECK_INTERVAL = 5f; private const float maxDistance = 100f; public static readonly List PlayerCache = new List(); private float _playerCacheTimer = 0f; private const float PLAYER_CACHE_INTERVAL = 1f; private void Update() { _playerCacheTimer += Time.deltaTime; if (_playerCacheTimer >= 1f) { _playerCacheTimer = 0f; RefreshPlayerCache(); } PruneUnits(); _checkTimer += Time.deltaTime; if (_checkTimer >= 5f) { PeriodicOfficerCheck(); _checkTimer = 0f; } } private static void RefreshPlayerCache() { PlayerCache.Clear(); try { List playerList = Player.PlayerList; if (playerList != null) { foreach (Player item in playerList) { if ((Object)(object)item != (Object)null) { PlayerCache.Add(item); } } return; } } catch { } PlayerCache.AddRange(Object.FindObjectsOfType(true)); } private static void PruneUnits() { for (int num = _units.Count - 1; num >= 0; num--) { K9UnitController k9UnitController = _units[num]; if ((Object)(object)k9UnitController == (Object)null || (Object)(object)k9UnitController.Officer == (Object)null || (Object)(object)((NPC)k9UnitController.Officer).Health == (Object)null || ((NPC)k9UnitController.Officer).Health.IsDead) { if ((Object)(object)k9UnitController != (Object)null) { string text = (((Object)(object)k9UnitController.Officer == (Object)null) ? "its officer no longer exists" : (((Object)(object)((NPC)k9UnitController.Officer).Health == (Object)null) ? "its officer has no Health component" : "its officer is dead")); Logger.Debug("K9 unit pruned: " + text + "."); Object.Destroy((Object)(object)((Component)k9UnitController).gameObject); } _units.RemoveAt(num); } } } public static PoliceOfficer FindClosestOfficer(Vector3 pos) { //IL_008e: Unknown result type (might be due to invalid IL or missing references) //IL_0096: Unknown result type (might be due to invalid IL or missing references) //IL_009b: Unknown result type (might be due to invalid IL or missing references) //IL_00a0: Unknown result type (might be due to invalid IL or missing references) float num = 10000f; PoliceOfficer result = null; HashSet hashSet = new HashSet(_units.Select((K9UnitController u) => u.Officer)); foreach (PoliceOfficer officer in PoliceOfficer.Officers) { if (!((Object)(object)officer == (Object)null) && !((Object)(object)((Component)officer).transform == (Object)null) && ((Behaviour)officer).isActiveAndEnabled && !hashSet.Contains(officer) && !IsOfficerInStation(officer)) { Vector3 val = pos - ((Component)officer).transform.position; float sqrMagnitude = ((Vector3)(ref val)).sqrMagnitude; if (sqrMagnitude < num) { num = sqrMagnitude; result = officer; } } } return result; } private static void PeriodicOfficerCheck() { if (!K9Net.IsServer()) { return; } foreach (K9UnitController unit in _units) { if ((Object)(object)unit != (Object)null && (Object)(object)unit.Officer != (Object)null) { K9NetSync.ServerEnsureRegistered(unit.Officer); } } int count = _units.Count; if (count < K9Config.MaxUnitCount && (Object)(object)Singleton.Instance != (Object)null && Singleton.Instance.Regions != null) { int count2 = K9Config.MaxUnitCount - count; SpawnOfficersWithK9InRandomRegions(count2); } } public static bool IsOfficerAssigned(PoliceOfficer officer) { if ((Object)(object)officer == (Object)null) { return true; } foreach (K9UnitController unit in _units) { if ((Object)(object)unit != (Object)null && (Object)(object)unit.Officer == (Object)(object)officer) { return true; } } return false; } public static bool IsOfficerInStation(PoliceOfficer officer) { if ((Object)(object)officer == (Object)null) { return false; } foreach (PoliceStation policeStation in PoliceStation.PoliceStations) { if ((Object)(object)policeStation != (Object)null && policeStation.OfficerPool.Contains(officer)) { return true; } } return false; } private static void SpawnOfficersWithK9InRandomRegions(int count) { //IL_00fe: Unknown result type (might be due to invalid IL or missing references) //IL_0103: Unknown result type (might be due to invalid IL or missing references) //IL_01d4: Unknown result type (might be due to invalid IL or missing references) //IL_0212: Unknown result type (might be due to invalid IL or missing references) //IL_0219: Expected O, but got Unknown //IL_0227: Unknown result type (might be due to invalid IL or missing references) Logger.Debug($"Spawning up to {count} K9 officer(s)..."); int num = 0; int num2 = 0; List policeStations = PoliceStation.PoliceStations; if (policeStations == null || policeStations.Count == 0) { Logger.Warning("No police stations found - cannot spawn K9 officers."); return; } FootPatrolRoute[] array = Object.FindObjectsOfType(); List list = new List(); if (array != null) { FootPatrolRoute[] array2 = array; foreach (FootPatrolRoute val in array2) { if ((Object)(object)val != (Object)null && val.Waypoints != null && val.StartWaypointIndex >= 0 && val.StartWaypointIndex < val.Waypoints.Length) { list.Add(val); } } } while (num < count && num2 < count * 3) { num2++; int index = Random.Range(0, policeStations.Count); PoliceStation val2 = policeStations[index]; if ((Object)(object)val2 == (Object)null) { continue; } Vector3 position = ((Component)val2).transform.position; try { FootPatrolRoute val3 = null; if (list.Count > 0) { val3 = list[Random.Range(0, list.Count)]; } PoliceOfficer val4 = null; if ((Object)(object)val3 != (Object)null) { PatrolGroup val5 = Singleton.Instance.StartFootpatrol(val3, 1); if (val5?.Members != null) { foreach (NPC member in val5.Members) { PoliceOfficer val6 = K9Cast.AsOfficer(member); if ((Object)(object)val6 != (Object)null && !IsOfficerAssigned(val6)) { val4 = val6; break; } } } } if (val4 == null) { val4 = FindClosestOfficer(position); } if ((Object)(object)val4 == (Object)null) { Logger.Warning("Failed to acquire officer for K9 unit - retrying."); continue; } GameObject val7 = new GameObject($"K9Unit_{((Object)val4).GetInstanceID()}"); val7.transform.position = ((Component)val4).transform.position; K9UnitController k9UnitController = val7.AddComponent(); k9UnitController.Initialize(val4); _units.Add(k9UnitController); K9NetSync.ServerSpawn(val4); num++; Logger.Debug($"Spawned K9 unit for officer {((Object)val4).GetInstanceID()} at station {((Object)val2).name}"); } catch (Exception arg) { Logger.Error($"SpawnOfficersWithK9InRandomRegions exception: {arg}"); } } if (num < count) { Logger.Warning($"Only {num} of {count} K9 officer(s) were spawned."); } } } } namespace K9_Patrol.Source.NPCs { public class K9NPC : MonoBehaviour { public enum K9State { Idle, Following, Investigating } internal static readonly List Live = new List(); private NavMeshAgent _agent; private Animator _anim; public PoliceOfficer _officer; public Player _pursuitTarget; private K9State _state; private float _lastWarpTime; private float _desiredSpeed; private const string SpeedParamName = "speed"; private const string IsWalkingParamName = "isWalking"; private const string IsTrackingParamName = "isTracking"; private const string BarkParamName = "bark"; private const string SitParamName = "sit"; private const string WagParamName = "wag"; private const string AlertParamName = "alert"; private const string PlayRateParamName = "playRate"; private const string OverlayLayerName = "Overlay"; private static readonly int SpeedHash = Animator.StringToHash("speed"); private static readonly int IsWalkingHash = Animator.StringToHash("isWalking"); private static readonly int IsTrackingHash = Animator.StringToHash("isTracking"); private static readonly int BarkHash = Animator.StringToHash("bark"); private static readonly int SitHash = Animator.StringToHash("sit"); private static readonly int WagHash = Animator.StringToHash("wag"); private static readonly int AlertHash = Animator.StringToHash("alert"); private static readonly int PlayRateHash = Animator.StringToHash("playRate"); private bool _animParamValidated; private bool _speedParamExists; private bool _isWalkingParamExists; private bool _isTrackingParamExists; private bool _barkParamExists; private bool _sitParamExists; private bool _wagParamExists; private bool _alertParamExists; private bool _playRateParamExists; private int _overlayLayer = -1; private float _overlayWeight; private const float ANIM_FULL_RUN_AT = 1.2f; private const float MIN_PLAY_RATE = 0.6f; private const float OVERLAY_BARK_HOLD = 1.2f; private const float OVERLAY_FADE = 6f; private float _overlayHoldUntil = -999f; private float _lastLoggedMotion; private const float SIT_AFTER_IDLE = 3f; private const float WAG_MIN_GAP = 11f; private const float WAG_MAX_GAP = 26f; private const float ALERT_ENTER_DISTANCE = 3.5f; private const float ALERT_EXIT_DISTANCE = 4.5f; private bool _closedIn; private float _idleSince = -1f; private float _nextWagAt; private float _lastBarkTime = -999f; private const float BARK_COOLDOWN = 3f; private float _lastAlertTime = -999f; private const float ALERT_COOLDOWN = 6f; private float _lastOwTime = -999f; private const float OW_COOLDOWN = 2f; private float _lastOwBroadcastAt = -999f; private AudioSource _audio; private float _nextAmbientBarkAt; private Vector3 _lastRequestedDestination = new Vector3(float.NaN, float.NaN, float.NaN); private float _nextRepathAt; private const float REPATH_INTERVAL = 0.2f; private const float REPATH_MOVE_THRESHOLD = 0.75f; private Vector3 _lastHandlerPos = new Vector3(float.NaN, float.NaN, float.NaN); private float _handlerSpeed; private const float HANDLER_MOVING_SPEED = 0.35f; private const float FOLLOW_ENTER_DISTANCE = 2.6f; private const float FOLLOW_EXIT_DISTANCE = 1.8f; private bool _offMeshReported; private const float STALL_TIMEOUT = 1.5f; private const float STALL_SPEED = 0.05f; private float _stalledSince = -1f; private static float AnimSpeedScale => Mathf.Max(0.05f, K9Config.DogAnimationSpeedScale.Value); private float CloseDistance => Mathf.Min(4f, K9DogTuning.CatchupStart * 0.65f); private bool HandlerIsMoving(Vector3 officerPos) { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_0092: Unknown result type (might be due to invalid IL or missing references) //IL_0093: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Unknown result type (might be due to invalid IL or missing references) if (float.IsNaN(_lastHandlerPos.x)) { _lastHandlerPos = officerPos; return false; } float deltaTime = Time.deltaTime; if (deltaTime > 0.0001f) { Vector3 val = officerPos - _lastHandlerPos; val.y = 0f; float num = ((Vector3)(ref val)).magnitude / deltaTime; if (num < 20f) { _handlerSpeed = Mathf.Lerp(_handlerSpeed, num, 1f - Mathf.Exp(-6f * deltaTime)); } } _lastHandlerPos = officerPos; return _handlerSpeed > 0.35f; } public void Initialize(PoliceOfficer officerGO, NavMeshAgent agent, Animator animator) { _officer = officerGO; _agent = agent; _anim = animator; if ((Object)(object)_agent != (Object)null) { _agent.stoppingDistance = 1.25f; _agent.autoBraking = true; _agent.updateRotation = true; } _desiredSpeed = K9DogTuning.BaseSpeed; ChangeState(K9State.Idle); EnsureAudioSource(); ValidateAnimatorParameter(); ScheduleNextAmbientBark(); K9DogHit.Attach(((Component)this).gameObject); if (!Live.Contains(this)) { Live.Add(this); } } private void OnDestroy() { Live.Remove(this); } private void ValidateAnimatorParameter() { _animParamValidated = true; if ((Object)(object)_anim == (Object)null) { Logger.Warning("K9NPC: Animator missing."); return; } if ((Object)(object)_anim.runtimeAnimatorController == (Object)null) { Logger.Warning("K9NPC: Animator has no runtimeAnimatorController assigned."); return; } for (int i = 0; i < _anim.layerCount; i++) { if (_anim.GetLayerName(i) == "Overlay") { _overlayLayer = i; break; } } if (_overlayLayer < 0) { Logger.Warning("K9NPC: no 'Overlay' animator layer. Sniff and growl will not play, and the bundle is older than this build."); } else { _anim.SetLayerWeight(_overlayLayer, 0f); } AnimatorControllerParameter[] parameters = _anim.parameters; AnimatorControllerParameter[] array = parameters; foreach (AnimatorControllerParameter val in array) { if (val != null) { if (val.name == "speed") { _speedParamExists = true; } if (val.name == "isWalking") { _isWalkingParamExists = true; } if (val.name == "isTracking") { _isTrackingParamExists = true; } if (val.name == "bark") { _barkParamExists = true; } if (val.name == "sit") { _sitParamExists = true; } if (val.name == "wag") { _wagParamExists = true; } if (val.name == "alert") { _alertParamExists = true; } if (val.name == "playRate") { _playRateParamExists = true; } } } if (!_speedParamExists) { Logger.Warning("K9NPC: Animator parameter 'speed' not found."); } if (!_isWalkingParamExists) { Logger.Warning("K9NPC: Animator parameter 'isWalking' not found."); } if (!_isTrackingParamExists) { Logger.Warning("K9NPC: Animator parameter 'isTracking' not found."); } if (!_barkParamExists) { Logger.Warning("K9NPC: Animator parameter 'bark' not found."); } if (!_sitParamExists || !_wagParamExists || !_alertParamExists) { Logger.Warning($"K9NPC: Animator is missing the b22 parameters (sit={_sitParamExists} " + $"wag={_wagParamExists} alert={_alertParamExists}). The bundle is older " + "than this build - rebuild k9model.bundle from the Unity project."); } if (!_playRateParamExists) { Logger.Warning("K9NPC: Animator parameter 'playRate' not found. The bundle is older than this build, and the dog will appear to SLIDE - its legs run at the clip's own pace while its body moves at the agent's. Rebuild k9model.bundle from the Unity project."); } } public void TriggerBark() { Bark("K9_Patrol.Assets.Woof.mp3", priority: false); } public void TriggerAlert() { Bark("K9_Patrol.Assets.TheyHaveDrugs.mp3", priority: true); } private void EnsureAudioSource() { if ((Object)(object)_audio != (Object)null) { return; } try { AudioSource component = ((Component)this).gameObject.GetComponent(); _audio = (((Object)(object)component != (Object)null) ? component : ((Component)this).gameObject.AddComponent()); if ((Object)(object)_audio == (Object)null) { Logger.Warning("K9NPC: could not attach an AudioSource to this dog; falling back to positional one-shots."); } else { K9Audio.Configure3D(_audio); } } catch (Exception ex) { Logger.Warning("K9NPC: could not set up an AudioSource (" + ex.Message + "); falling back to positional one-shots."); _audio = null; } } private void Bark(string resource, bool priority) { //IL_0098: Unknown result type (might be due to invalid IL or missing references) float time = Time.time; if (priority) { if (time - _lastAlertTime < 6f) { return; } } else if (time - _lastBarkTime < 3f) { return; } if (!priority && (Object)(object)_audio != (Object)null && _audio.isPlaying) { return; } EnsureAudioSource(); bool flag = K9Audio.PlayOn(_audio, resource); if (!flag) { flag = K9Audio.PlayAt(resource, ((Component)this).transform.position); } try { if (!_animParamValidated) { ValidateAnimatorParameter(); } _overlayHoldUntil = Time.time + 1.2f; if ((Object)(object)_anim != (Object)null && _barkParamExists) { _anim.SetTrigger(BarkHash); } } catch (Exception ex) { Logger.Debug("K9NPC.Bark animator failed: " + ex.Message); } if (flag) { if (priority) { _lastAlertTime = time; } else { _lastBarkTime = time; } } if (K9Config.DebugLogging.Value) { Logger.Debug($"K9NPC.Bark '{resource}' priority={priority} haveSource={(Object)(object)_audio != (Object)null} " + $"sourcePlaying={(Object)(object)_audio != (Object)null && _audio.isPlaying} played={flag}"); } } internal void OnHit(Impact impact) { if (impact != null) { PlayOw(); float time = Time.time; if (time - _lastOwBroadcastAt >= 2f) { _lastOwBroadcastAt = time; K9NetSync.BroadcastDogYelp(_officer); } K9DogHit.ForwardToOfficer(_officer, impact); } } internal void PlayRemoteYelp() { PlayOw(); } private void PlayOw() { //IL_004f: Unknown result type (might be due to invalid IL or missing references) float time = Time.time; if (!(time - _lastOwTime < 2f)) { EnsureAudioSource(); bool flag = K9Audio.PlayOn(_audio, "K9_Patrol.Assets.Ow.mp3"); if (!flag) { flag = K9Audio.PlayAt("K9_Patrol.Assets.Ow.mp3", ((Component)this).transform.position); } if (flag) { _lastOwTime = time; } if (K9Config.DebugLogging.Value) { Logger.Debug($"K9NPC.PlayOw haveSource={(Object)(object)_audio != (Object)null} played={flag}"); } } } private void ScheduleNextAmbientBark() { float num = Mathf.Max(1f, K9Config.AmbientBarkMinInterval.Value); float num2 = Mathf.Max(num, K9Config.AmbientBarkMaxInterval.Value); _nextAmbientBarkAt = Time.time + Random.Range(num, num2); } private void TryAmbientBark() { //IL_0061: Unknown result type (might be due to invalid IL or missing references) //IL_0066: Unknown result type (might be due to invalid IL or missing references) //IL_00a3: Unknown result type (might be due to invalid IL or missing references) //IL_00ac: Unknown result type (might be due to invalid IL or missing references) //IL_00b1: Unknown result type (might be due to invalid IL or missing references) //IL_00b6: Unknown result type (might be due to invalid IL or missing references) if (!K9Config.AmbientBarkEnabled.Value || (Object)(object)_pursuitTarget != (Object)null) { return; } float time = Time.time; if (time < _nextAmbientBarkAt) { return; } float value = K9Config.AmbientBarkRadius.Value; float num = value * value; bool flag = false; Vector3 position = ((Component)this).transform.position; foreach (Player item in K9Manager.PlayerCache) { if (!((Object)(object)item == (Object)null) && !((Object)(object)((Component)item).transform == (Object)null)) { Vector3 val = position - ((Component)item).transform.position; if (((Vector3)(ref val)).sqrMagnitude <= num) { flag = true; break; } } } if (!flag) { _nextAmbientBarkAt = time + 2f; return; } ScheduleNextAmbientBark(); TriggerBark(); } private void RequestPath(Vector3 destination) { //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0076: Unknown result type (might be due to invalid IL or missing references) //IL_0077: Unknown result type (might be due to invalid IL or missing references) //IL_0082: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)_agent == (Object)null || !_agent.isOnNavMesh) { return; } if (_agent.hasPath && Time.time < _nextRepathAt) { Vector3 val = destination - _lastRequestedDestination; if (((Vector3)(ref val)).sqrMagnitude < 0.5625f) { return; } } _nextRepathAt = Time.time + 0.2f; _lastRequestedDestination = destination; _agent.SetDestination(destination); } private void ChangeState(K9State next) { if (_state != next) { _state = next; if (_state == K9State.Idle) { _idleSince = Time.time; ScheduleNextWag(); } else { _idleSince = -1f; } Logger.Debug($"K9NPC state changed to {_state}"); } } private void ScheduleNextWag() { _nextWagAt = Time.time + Random.Range(11f, 26f); } private void Update() { //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_005c: Unknown result type (might be due to invalid IL or missing references) //IL_006a: Unknown result type (might be due to invalid IL or missing references) //IL_011a: Unknown result type (might be due to invalid IL or missing references) //IL_0124: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)_officer == (Object)null || (Object)(object)_agent == (Object)null) { return; } Vector3 position = ((Component)_officer).transform.position; float num = Vector3.Distance(((Component)this).transform.position, position); float verticalDelta = Mathf.Abs(((Component)this).transform.position.y - position.y); bool flag = HandlerIsMoving(position); if ((Object)(object)_pursuitTarget != (Object)null) { ChangeState(K9State.Investigating); } else { switch (_state) { case K9State.Idle: if (num > 2.6f || flag) { ChangeState(K9State.Following); } break; case K9State.Following: if (num < 1.8f && !flag) { ChangeState(K9State.Idle); } break; case K9State.Investigating: ChangeState(K9State.Idle); break; } } switch (_state) { case K9State.Investigating: HandleInvestigating(); break; case K9State.Following: HandleFollowing(num, position); break; default: HandleIdle(position); break; } UpdateAnimation(); TryAmbientBark(); TryHardWarp(num, verticalDelta); } private void HandleIdle(Vector3 officerPos) { //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_0080: Unknown result type (might be due to invalid IL or missing references) //IL_0081: Unknown result type (might be due to invalid IL or missing references) //IL_0086: Unknown result type (might be due to invalid IL or missing references) //IL_0093: 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) //IL_00a4: Unknown result type (might be due to invalid IL or missing references) if (_agent.hasPath) { _agent.ResetPath(); } _desiredSpeed = Mathf.MoveTowards(_desiredSpeed, 0f, Time.deltaTime * K9DogTuning.Acceleration); _agent.speed = _desiredSpeed; Vector3 val = officerPos - ((Component)this).transform.position; val.y = 0f; if (((Vector3)(ref val)).sqrMagnitude > 0.0005f) { Quaternion val2 = Quaternion.LookRotation(val); ((Component)this).transform.rotation = Quaternion.Slerp(((Component)this).transform.rotation, val2, Time.deltaTime * 4f); } } private void HandleFollowing(float dist, Vector3 officerPos) { //IL_00b5: Unknown result type (might be due to invalid IL or missing references) float baseSpeed = K9DogTuning.BaseSpeed; float catchupMax = K9DogTuning.CatchupMax; float num; if (dist < CloseDistance) { num = Mathf.Max(baseSpeed * 0.5f, _handlerSpeed); } else if (dist < K9DogTuning.CatchupStart) { num = baseSpeed; } else { float num2 = Mathf.Max(0.5f, K9DogTuning.HardWarpDistance - K9DogTuning.CatchupStart); float num3 = Mathf.Clamp01((dist - K9DogTuning.CatchupStart) / num2); num = baseSpeed * Mathf.Lerp(1f, catchupMax, num3); } _desiredSpeed = Mathf.MoveTowards(_desiredSpeed, num, Time.deltaTime * K9DogTuning.Acceleration); _agent.speed = Mathf.Max(0.01f, _desiredSpeed); RequestPath(officerPos); } private void HandleInvestigating() { //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Unknown result type (might be due to invalid IL or missing references) //IL_003b: Unknown result type (might be due to invalid IL or missing references) //IL_0078: Unknown result type (might be due to invalid IL or missing references) //IL_00b4: Unknown result type (might be due to invalid IL or missing references) //IL_00bb: Unknown result type (might be due to invalid IL or missing references) //IL_00c0: Unknown result type (might be due to invalid IL or missing references) //IL_00c5: Unknown result type (might be due to invalid IL or missing references) //IL_00e8: Unknown result type (might be due to invalid IL or missing references) //IL_00ea: Unknown result type (might be due to invalid IL or missing references) //IL_00ef: Unknown result type (might be due to invalid IL or missing references) //IL_00fd: Unknown result type (might be due to invalid IL or missing references) //IL_0102: Unknown result type (might be due to invalid IL or missing references) //IL_010f: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)_pursuitTarget == (Object)null) { ChangeState(K9State.Idle); return; } Vector3 position = ((Component)_pursuitTarget).transform.position; float num = Vector3.Distance(((Component)this).transform.position, position); float baseSpeed = K9DogTuning.BaseSpeed; _desiredSpeed = Mathf.MoveTowards(_desiredSpeed, baseSpeed, Time.deltaTime * K9DogTuning.Acceleration); _agent.speed = _desiredSpeed; RequestPath(position); if (num < _agent.stoppingDistance) { if (_agent.hasPath) { _agent.ResetPath(); } Vector3 val = position - ((Component)this).transform.position; val.y = 0f; if (((Vector3)(ref val)).sqrMagnitude > 0.0005f) { Quaternion val2 = Quaternion.LookRotation(val); ((Component)this).transform.rotation = Quaternion.Slerp(((Component)this).transform.rotation, val2, Time.deltaTime * 4f); } } } private bool IsStalled() { //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0196: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)_agent == (Object)null) { return false; } bool flag = _state != K9State.Idle && _desiredSpeed > 0.05f; Vector3 velocity = _agent.velocity; bool flag2 = ((Vector3)(ref velocity)).sqrMagnitude > 0.0025000002f; bool flag3 = !_agent.pathPending && _agent.remainingDistance <= _agent.stoppingDistance + 0.15f; if (!flag || flag2 || flag3) { if (_stalledSince >= 0f) { _stalledSince = -1f; if (_offMeshReported) { _offMeshReported = false; Logger.Debug("K9 dog is moving again."); } } return false; } if (_stalledSince < 0f) { _stalledSince = Time.time; return false; } if (Time.time - _stalledSince < 1.5f) { return false; } if (!_offMeshReported) { _offMeshReported = true; Logger.Warning($"K9 dog is STUCK - it wants to move at {_desiredSpeed:0.0} m/s and is not moving at all. " + $"onNavMesh={_agent.isOnNavMesh} hasPath={_agent.hasPath} pathPending={_agent.pathPending} " + $"pathStatus={_agent.pathStatus} state={_state}. Warping it back to its handler. " + "onNavMesh=False means nothing ever set a destination; a Partial or Invalid pathStatus means the handler is somewhere the dog cannot walk to."); } return true; } private void TryHardWarp(float dist, float verticalDelta) { //IL_0068: Unknown result type (might be due to invalid IL or missing references) //IL_006d: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Unknown result type (might be due to invalid IL or missing references) //IL_00cf: Unknown result type (might be due to invalid IL or missing references) //IL_008b: Unknown result type (might be due to invalid IL or missing references) float hardWarpDistance = K9DogTuning.HardWarpDistance; bool flag = IsStalled(); if (!flag && dist < hardWarpDistance && verticalDelta < K9DogTuning.HardWarpVertical) { return; } float time = Time.time; if (!(time - _lastWarpTime < K9DogTuning.WarpCooldown)) { _lastWarpTime = time; _stalledSince = -1f; Vector3 position = ((Component)_officer).transform.position; NavMeshHit val = default(NavMeshHit); if (NavMesh.SamplePosition(position, ref val, 3f, -1)) { _agent.Warp(((NavMeshHit)(ref val)).position); Logger.Debug("K9 dog hard-warped (" + (flag ? "it was stuck" : $"{dist:0.0}m behind its handler") + ")."); } else { ((Component)this).transform.position = position; Logger.Warning("K9 dog warp fallback (no navmesh sample)."); } } } private void UpdateAnimation() { //IL_009c: Unknown result type (might be due to invalid IL or missing references) //IL_00ac: Unknown result type (might be due to invalid IL or missing references) //IL_00b1: Unknown result type (might be due to invalid IL or missing references) //IL_00b6: Unknown result type (might be due to invalid IL or missing references) //IL_0282: Unknown result type (might be due to invalid IL or missing references) //IL_0287: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)_anim == (Object)null || (Object)(object)_agent == (Object)null) { return; } if (!_animParamValidated) { ValidateAnimatorParameter(); } bool flag = _state == K9State.Following || _state == K9State.Investigating; if (_isWalkingParamExists) { _anim.SetBool(IsWalkingHash, flag); } bool flag2 = (Object)(object)_pursuitTarget != (Object)null; Vector3 val; if (!flag2) { _closedIn = false; } else { val = ((Component)this).transform.position - ((Component)_pursuitTarget).transform.position; float sqrMagnitude = ((Vector3)(ref val)).sqrMagnitude; float num = (_closedIn ? 4.5f : 3.5f); _closedIn = sqrMagnitude <= num * num; } bool closedIn = _closedIn; if (_isTrackingParamExists) { _anim.SetBool(IsTrackingHash, flag2 && !closedIn); } if (_alertParamExists) { _anim.SetBool(AlertHash, closedIn); } if (_overlayLayer >= 0) { bool flag3 = flag2 || Time.time < _overlayHoldUntil; _overlayWeight = Mathf.MoveTowards(_overlayWeight, flag3 ? 1f : 0f, 6f * Time.deltaTime); _anim.SetLayerWeight(_overlayLayer, _overlayWeight); } bool flag4 = !flag2 && _state == K9State.Idle && _idleSince >= 0f; if (_sitParamExists) { _anim.SetBool(SitHash, flag4 && Time.time - _idleSince >= 3f); } if (_wagParamExists && flag4 && Time.time >= _nextWagAt) { if (Time.time - _idleSince < 3f) { _anim.SetTrigger(WagHash); } ScheduleNextWag(); } if (_speedParamExists) { float num2 = Mathf.Max(0.01f, K9DogTuning.BaseSpeed * 1.2f); val = _agent.velocity; float num3 = Mathf.Min(((Vector3)(ref val)).magnitude, Mathf.Max(0.01f, _agent.speed)); float num4 = num3 / num2; float num5 = ((num4 > 1f) ? 1f : num4); try { _anim.SetFloat("speed", num5, 0.12f, Time.deltaTime); } catch { _anim.SetFloat(SpeedHash, num5); } float num6 = 1f; if (_playRateParamExists) { num6 = Mathf.Clamp(AnimSpeedScale * num4, 0.6f, AnimSpeedScale * 2f); _anim.SetFloat(PlayRateHash, num6); } if (K9Config.DebugLogging.Value && Mathf.Abs(num5 - _lastLoggedMotion) > 0.05f) { Logger.Debug($"AnimSpeed motion={num5:0.00} vel={num3:0.00} agentSpeed={_agent.speed:0.00} " + $"playRate={num6:0.00} (scale {AnimSpeedScale:0.00})"); _lastLoggedMotion = num5; } } } } internal static class K9DogHit { private const string HitLayerName = "NPCTrigger"; private static readonly Vector3 MinSize = new Vector3(0.15f, 0.15f, 0.3f); private static readonly Vector3 MaxSize = new Vector3(2f, 2f, 3f); private const float Padding = 1.1f; private static readonly Vector3 FallbackSize = new Vector3(0.38610002f, 0.88000005f, 1.1572001f); private static readonly Vector3 FallbackCentre = new Vector3(0f, 0.4f, 0.097f); private static bool _patched; private static bool _patchFailed; private static bool _masksChecked; internal static void Install(Harmony harmony) { //IL_008d: Unknown result type (might be due to invalid IL or missing references) //IL_009a: Expected O, but got Unknown if (_patched || _patchFailed) { return; } try { if (harmony == null) { Logger.Warning("K9DogHit: no Harmony instance; the dog will not be hittable."); _patchFailed = true; return; } MethodInfo methodInfo = AccessTools.Method(typeof(PhysicsDamageable), "ReceiveImpact", (Type[])null, (Type[])null); if (methodInfo == null) { Logger.Warning("K9DogHit: PhysicsDamageable.ReceiveImpact not found - the game's combat API has moved. The dog will not be hittable."); _patchFailed = true; return; } MethodInfo methodInfo2 = AccessTools.Method(typeof(K9DogHit), "ReceiveImpactPostfix", (Type[])null, (Type[])null); harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(methodInfo2), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); _patched = true; Logger.Msg("K9DogHit: patched PhysicsDamageable.ReceiveImpact - the dog is hittable."); } catch (Exception ex) { _patchFailed = true; Logger.Error("K9DogHit: could not patch PhysicsDamageable.ReceiveImpact (" + ex.Message + "). The dog will not be hittable; everything else is unaffected."); } } internal static void Attach(GameObject dog) { //IL_00e6: Unknown result type (might be due to invalid IL or missing references) //IL_00ef: Unknown result type (might be due to invalid IL or missing references) //IL_011c: 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) //IL_0130: Unknown result type (might be due to invalid IL or missing references) //IL_00cb: Unknown result type (might be due to invalid IL or missing references) //IL_00d0: Unknown result type (might be due to invalid IL or missing references) //IL_00d2: Unknown result type (might be due to invalid IL or missing references) //IL_00d7: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)dog == (Object)null) { return; } try { int num = LayerMask.NameToLayer("NPCTrigger"); if (num < 0) { Logger.Warning("K9DogHit: no 'NPCTrigger' layer in this build of the game; the dog will not be hittable."); return; } PhysicsDamageable val = dog.GetComponent(); if ((Object)(object)val == (Object)null) { val = dog.AddComponent(); } if ((Object)(object)val == (Object)null) { Logger.Warning("K9DogHit: could not attach PhysicsDamageable; the dog will not be hittable."); return; } val.Rb = null; BoxCollider val2 = dog.GetComponent(); if ((Object)(object)val2 == (Object)null) { val2 = dog.AddComponent(); } if ((Object)(object)val2 == (Object)null) { Logger.Warning("K9DogHit: could not attach a BoxCollider; the dog will not be hittable."); return; } if (!TryMeasure(dog.transform, out var centre, out var size)) { centre = FallbackCentre; size = FallbackSize; Logger.Debug("K9DogHit: could not measure the dog; using the fallback hitbox."); } val2.center = centre; val2.size = size; ((Collider)val2).isTrigger = true; dog.layer = num; Logger.Debug("K9DogHit: hitbox on '" + ((Object)dog).name + "' layer=NPCTrigger " + $"centre={centre} size={size} (local units, root scale {dog.transform.localScale.x:0.00})."); CheckCombatMasksOnce(num); } catch (Exception ex) { Logger.Warning("K9DogHit.Attach failed (" + ex.Message + "); the dog will not be hittable."); } } private static void CheckCombatMasksOnce(int layer) { if (_masksChecked) { return; } _masksChecked = true; try { if (!NetworkSingleton.InstanceExists) { _masksChecked = false; return; } CombatManager instance = NetworkSingleton.Instance; if ((Object)(object)instance == (Object)null) { _masksChecked = false; return; } int num = 1 << layer; bool flag = (((LayerMask)(ref instance.MeleeLayerMask)).value & num) != 0; bool flag2 = (((LayerMask)(ref instance.RangedWeaponLayerMask)).value & num) != 0; if (flag && flag2) { Logger.Debug(string.Format("K9DogHit: '{0}' (layer {1}) is in both combat masks - ", "NPCTrigger", layer) + "punches, melee weapons and gunfire all reach the dog."); return; } Logger.Warning(string.Format("K9DogHit: '{0}' (layer {1}) is NOT in ", "NPCTrigger", layer) + (flag ? "" : "MeleeLayerMask") + ((flag || flag2) ? "" : " and ") + (flag2 ? "" : "RangedWeaponLayerMask") + ". The dog will not be hittable by those, and the assumption this mod makes about the game's own NPC hit layer no longer holds."); } catch (Exception ex) { Logger.Debug("K9DogHit: could not read the combat layer masks (" + ex.Message + ")."); } } private static bool TryMeasure(Transform root, out Vector3 centre, out Vector3 size) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0221: Unknown result type (might be due to invalid IL or missing references) //IL_022b: Unknown result type (might be due to invalid IL or missing references) //IL_0230: Unknown result type (might be due to invalid IL or missing references) //IL_0236: Unknown result type (might be due to invalid IL or missing references) //IL_023b: Unknown result type (might be due to invalid IL or missing references) //IL_0244: Unknown result type (might be due to invalid IL or missing references) //IL_0256: Unknown result type (might be due to invalid IL or missing references) //IL_0268: Unknown result type (might be due to invalid IL or missing references) //IL_0279: Unknown result type (might be due to invalid IL or missing references) //IL_028c: Unknown result type (might be due to invalid IL or missing references) //IL_029f: Unknown result type (might be due to invalid IL or missing references) //IL_00c3: 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_00cc: Unknown result type (might be due to invalid IL or missing references) //IL_00d1: Unknown result type (might be due to invalid IL or missing references) //IL_02c0: Unknown result type (might be due to invalid IL or missing references) //IL_02d3: Unknown result type (might be due to invalid IL or missing references) //IL_010a: Unknown result type (might be due to invalid IL or missing references) //IL_01a1: Unknown result type (might be due to invalid IL or missing references) //IL_01a3: Unknown result type (might be due to invalid IL or missing references) //IL_01a5: Unknown result type (might be due to invalid IL or missing references) //IL_01aa: Unknown result type (might be due to invalid IL or missing references) //IL_01ac: Unknown result type (might be due to invalid IL or missing references) //IL_01b3: Unknown result type (might be due to invalid IL or missing references) //IL_01bb: Unknown result type (might be due to invalid IL or missing references) //IL_02e6: Unknown result type (might be due to invalid IL or missing references) //IL_011e: 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_01d9: Unknown result type (might be due to invalid IL or missing references) //IL_01db: Unknown result type (might be due to invalid IL or missing references) //IL_01dd: Unknown result type (might be due to invalid IL or missing references) //IL_01e7: Unknown result type (might be due to invalid IL or missing references) //IL_01ec: Unknown result type (might be due to invalid IL or missing references) //IL_01f2: Unknown result type (might be due to invalid IL or missing references) //IL_01f4: Unknown result type (might be due to invalid IL or missing references) //IL_0135: Unknown result type (might be due to invalid IL or missing references) //IL_012c: Unknown result type (might be due to invalid IL or missing references) //IL_014c: Unknown result type (might be due to invalid IL or missing references) //IL_0143: Unknown result type (might be due to invalid IL or missing references) //IL_0154: Unknown result type (might be due to invalid IL or missing references) //IL_0159: Unknown result type (might be due to invalid IL or missing references) //IL_015e: Unknown result type (might be due to invalid IL or missing references) //IL_0168: Unknown result type (might be due to invalid IL or missing references) //IL_016a: Unknown result type (might be due to invalid IL or missing references) //IL_016f: Unknown result type (might be due to invalid IL or missing references) //IL_0174: Unknown result type (might be due to invalid IL or missing references) //IL_0176: Unknown result type (might be due to invalid IL or missing references) //IL_0178: Unknown result type (might be due to invalid IL or missing references) //IL_017a: Unknown result type (might be due to invalid IL or missing references) //IL_017f: Unknown result type (might be due to invalid IL or missing references) //IL_0181: Unknown result type (might be due to invalid IL or missing references) //IL_0183: Unknown result type (might be due to invalid IL or missing references) //IL_0185: Unknown result type (might be due to invalid IL or missing references) //IL_018a: Unknown result type (might be due to invalid IL or missing references) centre = Vector3.zero; size = Vector3.zero; Renderer[] componentsInChildren = ((Component)root).GetComponentsInChildren(true); if (componentsInChildren == null || componentsInChildren.Length == 0) { return false; } bool flag = false; float num = 0f; Renderer[] array = componentsInChildren; Vector3 val4 = default(Vector3); Vector3 val5 = default(Vector3); foreach (Renderer val in array) { if ((Object)(object)val == (Object)null) { continue; } Mesh val2 = null; SkinnedMeshRenderer val3 = K9Cast.AsSkinnedMesh(val); if ((Object)(object)val3 != (Object)null) { val2 = val3.sharedMesh; } else { MeshFilter component = ((Component)val).GetComponent(); if ((Object)(object)component != (Object)null) { val2 = component.sharedMesh; } } if (!((Object)(object)val2 == (Object)null)) { Bounds bounds = val2.bounds; Vector3 extents = ((Bounds)(ref bounds)).extents; ((Vector3)(ref val4))..ctor(float.MaxValue, float.MaxValue, float.MaxValue); ((Vector3)(ref val5))..ctor(float.MinValue, float.MinValue, float.MinValue); for (int j = 0; j < 8; j++) { Vector3 val6 = ((Bounds)(ref bounds)).center + new Vector3(((j & 1) == 0) ? (0f - extents.x) : extents.x, ((j & 2) == 0) ? (0f - extents.y) : extents.y, ((j & 4) == 0) ? (0f - extents.z) : extents.z); Vector3 val7 = root.InverseTransformPoint(((Component)val).transform.TransformPoint(val6)); val4 = Vector3.Min(val4, val7); val5 = Vector3.Max(val5, val7); } Vector3 val8 = val5 - val4; float num2 = val8.x * val8.y * val8.z; if (!(num2 <= num)) { num = num2; centre = (val4 + val5) * 0.5f; size = val8; flag = true; } } } if (!flag) { return false; } size *= 1.1f; Vector3 localScale = root.localScale; Vector3 val9 = default(Vector3); ((Vector3)(ref val9))..ctor(size.x * Mathf.Abs(localScale.x), size.y * Mathf.Abs(localScale.y), size.z * Mathf.Abs(localScale.z)); if (val9.x < MinSize.x || val9.y < MinSize.y || val9.z < MinSize.z) { return false; } if (val9.x > MaxSize.x || val9.y > MaxSize.y || val9.z > MaxSize.z) { return false; } return true; } private static void ReceiveImpactPostfix(PhysicsDamageable __instance, Impact impact) { try { if ((Object)(object)__instance == (Object)null || impact == null) { return; } GameObject gameObject = ((Component)__instance).gameObject; if (!((Object)(object)gameObject == (Object)null)) { K9NPC component = gameObject.GetComponent(); if (!((Object)(object)component == (Object)null)) { component.OnHit(impact); } } } catch (Exception ex) { Logger.Debug("K9DogHit postfix threw: " + ex.Message); } } internal static void ForwardToOfficer(PoliceOfficer officer, Impact impact) { //IL_0068: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)officer == (Object)null || impact == null) { return; } try { NPCResponses responses = ((NPC)officer).Responses; if ((Object)(object)responses == (Object)null) { Logger.Debug("K9DogHit: the handler has no NPCResponses; the hit was not reported."); return; } responses.ImpactReceived(impact); if (K9Config.DebugLogging.Value) { Player val = default(Player); Logger.Debug($"K9DogHit: forwarded a {impact.ImpactType} impact " + $"(dmg={impact.ImpactDamage:0.#} force={impact.ImpactForce:0.#}) to officer " + ((Object)officer).name + "; attacker=" + ((impact.IsPlayerImpact(ref val) && (Object)(object)val != (Object)null) ? val.PlayerCode : "not a player") + "."); } } catch (Exception ex) { Logger.Warning("K9DogHit: forwarding the hit to the officer failed (" + ex.Message + ")."); } } } internal static class K9DogFactory { private static bool _bundleMissWarned; private static bool _prefabMissWarned; internal static K9NPC Build(PoliceOfficer officer, string dogName) { //IL_006f: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Unknown result type (might be due to invalid IL or missing references) //IL_0084: Unknown result type (might be due to invalid IL or missing references) //IL_0089: 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_008f: Unknown result type (might be due to invalid IL or missing references) //IL_0090: Unknown result type (might be due to invalid IL or missing references) //IL_0092: Unknown result type (might be due to invalid IL or missing references) //IL_00a8: 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) //IL_00c2: Unknown result type (might be due to invalid IL or missing references) //IL_00ca: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)officer == (Object)null) { return null; } try { if ((Object)(object)Core.bundle == (Object)null) { WarnOnce(ref _bundleMissWarned, "K9DogFactory: the asset bundle is not loaded; no dog can be built."); return null; } GameObject val = K9Assets.LoadPrefab(); if ((Object)(object)val == (Object)null) { WarnOnce(ref _prefabMissWarned, "K9DogFactory: the dog prefab is missing from the bundle; no dog can be built."); return null; } Vector3 val2 = ((Component)officer).transform.position + ((Component)officer).transform.right * 0.9f; Vector3 val3 = val2; NavMeshHit val4 = default(NavMeshHit); if (NavMesh.SamplePosition(val2, ref val4, 3f, -1)) { val3 = ((NavMeshHit)(ref val4)).position; } else { val3.y += 0.5f; } GameObject val5 = Object.Instantiate(val, val3, ((Component)officer).transform.rotation); ((Object)val5).name = dogName; K9UnitController.ApplyDogMaterialAndVisualTweaks(val5); NavMeshAgent val6 = val5.GetComponent(); if ((Object)(object)val6 == (Object)null) { val6 = val5.AddComponent(); } val6.speed = K9DogTuning.BaseSpeed; val6.acceleration = K9DogTuning.Acceleration; val6.angularSpeed = K9DogTuning.AngularSpeed; Animator componentInChildren = val5.GetComponentInChildren(); K9NPC k9NPC = val5.AddComponent(); k9NPC.Initialize(officer, val6, componentInChildren); return k9NPC; } catch (Exception arg) { Logger.Error($"K9DogFactory.Build failed for officer '{((Object)officer).name}': {arg}"); return null; } } private static void WarnOnce(ref bool flag, string message) { if (!flag) { flag = true; Logger.Error(message); } } } } namespace K9_Patrol.Source.Utils { internal static class K9DogTuning { private const int PAYLOAD_VERSION = 1; private static bool _hasHost; private static float _baseSpeed; private static float _accel; private static float _angular; private static float _catchStart; private static float _catchMax; private static float _warpDist; private static float _warpVert; private static float _warpCooldown; internal static bool UsingHostValues => _hasHost; internal static float BaseSpeed => _hasHost ? _baseSpeed : K9Config.DogBaseSpeed.Value; internal static float Acceleration => _hasHost ? _accel : K9Config.DogAcceleration.Value; internal static float AngularSpeed => _hasHost ? _angular : K9Config.DogAngularSpeed.Value; internal static float CatchupStart => _hasHost ? _catchStart : K9Config.CatchupStartDistance.Value; internal static float CatchupMax => _hasHost ? _catchMax : K9Config.CatchupMaxMultiplier.Value; internal static float HardWarpDistance => _hasHost ? _warpDist : K9Config.HardWarpDistance.Value; internal static float HardWarpVertical => _hasHost ? _warpVert : K9Config.HardWarpVerticalDelta.Value; internal static float WarpCooldown => _hasHost ? _warpCooldown : K9Config.WarpCooldown.Value; internal static string BuildHostPayload() { CultureInfo invariantCulture = CultureInfo.InvariantCulture; return string.Join("|", 1.ToString(invariantCulture), K9Config.DogBaseSpeed.Value.ToString("R", invariantCulture), K9Config.DogAcceleration.Value.ToString("R", invariantCulture), K9Config.DogAngularSpeed.Value.ToString("R", invariantCulture), K9Config.CatchupStartDistance.Value.ToString("R", invariantCulture), K9Config.CatchupMaxMultiplier.Value.ToString("R", invariantCulture), K9Config.HardWarpDistance.Value.ToString("R", invariantCulture), K9Config.HardWarpVerticalDelta.Value.ToString("R", invariantCulture), K9Config.WarpCooldown.Value.ToString("R", invariantCulture)); } internal static void ApplyHostPayload(string payload) { if (string.IsNullOrEmpty(payload)) { return; } try { string[] array = payload.Split('|'); if (array.Length != 9) { Logger.Warning($"K9 tuning payload has {array.Length} fields, expected 9 - ignoring, staying on local config."); return; } CultureInfo invariantCulture = CultureInfo.InvariantCulture; if (!int.TryParse(array[0], NumberStyles.Integer, invariantCulture, out var result) || result != 1) { Logger.Warning($"K9 tuning payload version '{array[0]}' is not {1} - ignoring, staying on local config."); return; } if (!float.TryParse(array[1], NumberStyles.Float, invariantCulture, out var result2) || !float.TryParse(array[2], NumberStyles.Float, invariantCulture, out var result3) || !float.TryParse(array[3], NumberStyles.Float, invariantCulture, out var result4) || !float.TryParse(array[4], NumberStyles.Float, invariantCulture, out var result5) || !float.TryParse(array[5], NumberStyles.Float, invariantCulture, out var result6) || !float.TryParse(array[6], NumberStyles.Float, invariantCulture, out var result7) || !float.TryParse(array[7], NumberStyles.Float, invariantCulture, out var result8) || !float.TryParse(array[8], NumberStyles.Float, invariantCulture, out var result9)) { Logger.Warning("K9 tuning payload did not parse - ignoring, staying on local config."); return; } _baseSpeed = Clamp(result2, 0.1f, 50f); _accel = Clamp(result3, 0.1f, 200f); _angular = Clamp(result4, 10f, 5000f); _catchStart = Clamp(result5, 0.5f, 200f); _catchMax = Clamp(result6, 1f, 10f); _warpDist = Clamp(result7, 1f, 500f); _warpVert = Clamp(result8, 0.5f, 200f); _warpCooldown = Clamp(result9, 0f, 60f); _hasHost = true; Logger.Msg($"K9 dog tuning applied from host: baseSpeed={_baseSpeed} accel={_accel} angular={_angular} " + $"catchStart={_catchStart} catchMax={_catchMax} hardWarp={_warpDist} warpVert={_warpVert} warpCd={_warpCooldown}"); } catch (Exception ex) { Logger.Warning("K9 tuning payload threw while parsing (" + ex.Message + ") - staying on local config."); } } internal static void Clear() { if (_hasHost) { _hasHost = false; Logger.Debug("K9 dog tuning cleared - back to local config values."); } } private static float Clamp(float v, float lo, float hi) { return (v < lo) ? lo : ((v > hi) ? hi : v); } } public static class AssetBundleUtils { private static readonly Core mod = MelonAssembly.FindMelonInstance(); private static readonly MelonAssembly melonAssembly = ((MelonBase)mod).MelonAssembly; public static AssetBundle LoadAssetBundle(string logicalName) { try { string[] manifestResourceNames = melonAssembly.Assembly.GetManifestResourceNames(); string text = manifestResourceNames.FirstOrDefault((string n) => n.Equals(logicalName, StringComparison.OrdinalIgnoreCase)); if (text == null) { string alt = logicalName.Replace(' ', '_'); text = manifestResourceNames.FirstOrDefault((string n) => n.Equals(alt, StringComparison.OrdinalIgnoreCase)); } if (text == null) { text = manifestResourceNames.FirstOrDefault((string n) => n.EndsWith(logicalName, StringComparison.OrdinalIgnoreCase)); } if (text == null) { string fileOnly = logicalName.Split('.', StringSplitOptions.RemoveEmptyEntries).Last(); text = manifestResourceNames.FirstOrDefault((string n) => n.EndsWith(fileOnly, StringComparison.OrdinalIgnoreCase)); } if (text == null) { string text2 = string.Join("\n - ", manifestResourceNames); ((MelonBase)mod).Unregister("AssetBundle resource '" + logicalName + "' not found.\nAvailable embedded resources:\n - " + text2 + "\nExpected format: .Assets.k9model.bundle (RootNamespace = " + typeof(Core).Namespace + ")", false); return null; } Stream manifestResourceStream = melonAssembly.Assembly.GetManifestResourceStream(text); if (manifestResourceStream == null) { ((MelonBase)mod).Unregister("Found resource name '" + text + "' but stream was null.", false); return null; } return AssetBundle.LoadFromStream(manifestResourceStream); } catch (Exception arg) { ((MelonBase)mod).Unregister($"Failed to load AssetBundle. Please report to dev: {arg}", false); return null; } } public static string ResolveAssetName(AssetBundle bundle, string wantedPath) { if ((Object)(object)bundle == (Object)null || string.IsNullOrEmpty(wantedPath)) { return null; } try { string[] allAssetNames = bundle.GetAllAssetNames(); if (allAssetNames == null || allAssetNames.Length == 0) { Logger.Error("ResolveAssetName: the bundle reports no assets at all."); return null; } string[] array = allAssetNames; foreach (string text in array) { if (string.Equals(text, wantedPath, StringComparison.Ordinal)) { return text; } } string[] array2 = allAssetNames; foreach (string text2 in array2) { if (string.Equals(text2, wantedPath, StringComparison.OrdinalIgnoreCase)) { return text2; } } string b = wantedPath.Substring(wantedPath.LastIndexOf('/') + 1); string[] array3 = allAssetNames; foreach (string text3 in array3) { string a = text3.Substring(text3.LastIndexOf('/') + 1); if (string.Equals(a, b, StringComparison.OrdinalIgnoreCase)) { Logger.Warning("Asset '" + wantedPath + "' was not found at that path; matched '" + text3 + "' by file name instead. The bundle layout has changed - update the hardcoded path."); return text3; } } Logger.Error($"Asset '{wantedPath}' is not in the bundle. It holds {allAssetNames.Length} asset(s):\n - " + string.Join("\n - ", allAssetNames)); return null; } catch (Exception ex) { Logger.Error("ResolveAssetName('" + wantedPath + "') threw: " + ex.Message); return null; } } } internal static class K9Assets { internal const string Prefab = "Assets/Bublisher/3D Stylized Animated Dogs Kit/Prefabs/K9Model.prefab"; internal const string AlbedoTexture = "Assets/Bublisher/3D Stylized Animated Dogs Kit/Textures/3D Stylized Animated Dogs Kit - BaseColor_Custom.png"; internal const string NormalTexture = "Assets/Bublisher/3D Stylized Animated Dogs Kit/Textures/3D Stylized Animated Dogs Kit - Normal.png"; internal const string BarkClip = "Assets/ModContent/K9/Audio/Woof.mp3"; internal const string AlertClip = "Assets/ModContent/K9/Audio/TheyHaveDrugs.mp3"; internal const string OwClip = "Assets/ModContent/K9/Audio/ow.mp3"; internal static GameObject LoadPrefab() { if ((Object)(object)Core.bundle == (Object)null) { Logger.Error("LoadPrefab: bundle is null."); return null; } string text = AssetBundleUtils.ResolveAssetName(Core.bundle, "Assets/Bublisher/3D Stylized Animated Dogs Kit/Prefabs/K9Model.prefab"); return (text == null) ? null : Core.bundle.LoadAsset(text); } internal static Texture2D LoadTexture(string path) { if ((Object)(object)Core.bundle == (Object)null) { return null; } string text = AssetBundleUtils.ResolveAssetName(Core.bundle, path); return (text == null) ? null : Core.bundle.LoadAsset(text); } internal static AudioClip LoadAudio(string path) { if ((Object)(object)Core.bundle == (Object)null) { return null; } try { string[] allAssetNames = Core.bundle.GetAllAssetNames(); if (allAssetNames == null) { return null; } string b = path.Substring(path.LastIndexOf('/') + 1); string[] array = allAssetNames; foreach (string text in array) { string a = text.Substring(text.LastIndexOf('/') + 1); if (string.Equals(a, b, StringComparison.OrdinalIgnoreCase)) { return Core.bundle.LoadAsset(text); } } } catch (Exception ex) { Logger.Debug("K9Assets.LoadAudio('" + path + "') failed: " + ex.Message); } return null; } } internal static class K9Audio { internal const string BarkResource = "K9_Patrol.Assets.Woof.mp3"; internal const string AlertResource = "K9_Patrol.Assets.TheyHaveDrugs.mp3"; internal const string OwResource = "K9_Patrol.Assets.Ow.mp3"; internal const float GainDb = 3f; private static readonly Dictionary _clips = new Dictionary(); private static readonly HashSet _loading = new HashSet(); private static readonly HashSet _failed = new HashSet(); internal static float GainLinear => Mathf.Pow(10f, 0.15f); private static string CacheDir => Path.Combine(MelonEnvironment.UserDataDirectory, "DropDaDeuce-K9Patrol", "cache"); internal static void Configure3D(AudioSource source) { if (!((Object)(object)source == (Object)null)) { source.playOnAwake = false; source.loop = false; source.spatialBlend = 1f; source.rolloffMode = (AudioRolloffMode)0; source.minDistance = GainLinear; source.maxDistance = 500f; } } internal static void Preload() { EnsureLoaded("K9_Patrol.Assets.Woof.mp3"); EnsureLoaded("K9_Patrol.Assets.TheyHaveDrugs.mp3"); EnsureLoaded("K9_Patrol.Assets.Ow.mp3"); } private static bool TryLoadFromBundle(string resourceName) { //IL_013c: 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_00cf: Unknown result type (might be due to invalid IL or missing references) //IL_006c: Unknown result type (might be due to invalid IL or missing references) //IL_0072: Invalid comparison between Unknown and I4 string text = resourceName switch { "K9_Patrol.Assets.Ow.mp3" => "Assets/ModContent/K9/Audio/ow.mp3", "K9_Patrol.Assets.TheyHaveDrugs.mp3" => "Assets/ModContent/K9/Audio/TheyHaveDrugs.mp3", "K9_Patrol.Assets.Woof.mp3" => "Assets/ModContent/K9/Audio/Woof.mp3", _ => null, }; if (text == null) { return false; } AudioClip val = K9Assets.LoadAudio(text); if ((Object)(object)val == (Object)null) { return false; } try { if ((int)val.loadState != 2) { val.LoadAudioData(); } } catch (Exception ex) { Logger.Debug("K9Audio: LoadAudioData for '" + resourceName + "' threw (" + ex.Message + "); the first play may be silent."); } try { ((Object)val).hideFlags = (HideFlags)(((Object)val).hideFlags | 0x20); Object.DontDestroyOnLoad((Object)(object)val); } catch (Exception ex2) { Logger.Debug("K9Audio: could not pin '" + resourceName + "' against asset unloading (" + ex2.Message + ")."); } _clips[resourceName] = val; Logger.Debug($"K9Audio: loaded '{resourceName}' from the asset bundle ({val.length:0.00}s, state={val.loadState})."); return true; } internal static bool PlayOn(AudioSource source, string resourceName, float volume = 1f) { if ((Object)(object)source == (Object)null) { return false; } try { AudioClip clip = GetClip(resourceName); if ((Object)(object)clip == (Object)null) { return false; } source.Stop(); source.clip = clip; source.volume = Mathf.Clamp01(volume); source.Play(); return source.isPlaying; } catch (Exception ex) { Logger.Debug("K9Audio.PlayOn('" + resourceName + "') failed: " + ex.Message); return false; } } internal static bool PlayAt(string resourceName, Vector3 position, float volume = 1f) { //IL_0019: Unknown result type (might be due to invalid IL or missing references) try { AudioClip clip = GetClip(resourceName); if ((Object)(object)clip == (Object)null) { return false; } AudioSource.PlayClipAtPoint(clip, position, Mathf.Clamp01(volume)); return true; } catch (Exception ex) { Logger.Debug("K9Audio.PlayAt('" + resourceName + "') failed: " + ex.Message); return false; } } private static AudioClip GetClip(string resourceName) { if (_clips.TryGetValue(resourceName, out var value)) { if ((Object)(object)value != (Object)null) { return value; } _clips.Remove(resourceName); _failed.Remove(resourceName); Logger.Debug("K9Audio: '" + resourceName + "' was unloaded by the engine; reloading it."); } EnsureLoaded(resourceName); return (_clips.TryGetValue(resourceName, out value) && (Object)(object)value != (Object)null) ? value : null; } private static void EnsureLoaded(string resourceName) { if (!_clips.ContainsKey(resourceName) && !_loading.Contains(resourceName) && !_failed.Contains(resourceName) && !TryLoadFromBundle(resourceName)) { _loading.Add(resourceName); MelonCoroutines.Start(LoadRoutine(resourceName)); } } private static IEnumerator LoadRoutine(string resourceName) { string path; try { Directory.CreateDirectory(CacheDir); path = Path.Combine(CacheDir, resourceName); Core core = MelonAssembly.FindMelonInstance(); object obj; if (core == null) { obj = null; } else { MelonAssembly melonAssembly = ((MelonBase)core).MelonAssembly; obj = ((melonAssembly != null) ? melonAssembly.Assembly : null); } using Stream src = ((Assembly)obj)?.GetManifestResourceStream(resourceName); if (src == null) { Logger.Warning("K9Audio: embedded resource '" + resourceName + "' not found; that sound will be silent."); _failed.Add(resourceName); _loading.Remove(resourceName); yield break; } using FileStream dst = File.Create(path); src.CopyTo(dst); } catch (Exception ex) { Exception ex2 = ex; Logger.Warning("K9Audio: could not stage '" + resourceName + "' to disk (" + ex2.Message + "); that sound will be silent."); _failed.Add(resourceName); _loading.Remove(resourceName); yield break; } string url = new Uri(path).AbsoluteUri; UnityWebRequest req = UnityWebRequestMultimedia.GetAudioClip(url, (AudioType)13); yield return req.SendWebRequest(); try { if ((int)req.result != 1) { Logger.Warning("K9Audio: failed to decode '" + resourceName + "' (" + req.error + "); that sound will be silent."); _failed.Add(resourceName); yield break; } AudioClip clip = DownloadHandlerAudioClip.GetContent(req); if ((Object)(object)clip == (Object)null) { Logger.Warning("K9Audio: '" + resourceName + "' decoded to a null clip; that sound will be silent."); _failed.Add(resourceName); yield break; } Object.DontDestroyOnLoad((Object)(object)clip); ((Object)clip).hideFlags = (HideFlags)61; _clips[resourceName] = clip; Logger.Debug($"K9Audio: loaded '{resourceName}' ({clip.length:0.00}s)"); } catch (Exception ex) { Exception ex3 = ex; Logger.Warning("K9Audio: error finishing '" + resourceName + "' (" + ex3.Message + "); that sound will be silent."); _failed.Add(resourceName); } finally { _loading.Remove(resourceName); try { req.Dispose(); } catch { } } } } internal static class K9Net { private static NetworkManager _nm; private static float _nextResolveAt; private const float RESOLVE_INTERVAL = 1f; internal static NetworkManager Manager { get { if ((Object)(object)_nm != (Object)null) { return _nm; } if (Time.unscaledTime < _nextResolveAt) { return null; } _nextResolveAt = Time.unscaledTime + 1f; try { _nm = InstanceFinder.NetworkManager; } catch { } if ((Object)(object)_nm == (Object)null) { try { _nm = Object.FindObjectOfType(); } catch { } } return _nm; } } internal static bool IsServer() { try { NetworkManager manager = Manager; return (Object)(object)manager == (Object)null || manager.IsServer; } catch { return true; } } } internal static class K9ScentTable { private const string FileName = "K9Scents.cfg"; private const string SecSmell = "[smell]"; private const string SecPackaging = "[packaging]"; private const string SecItems = "[items]"; private const string SecContainers = "[containers]"; internal const string BackpackKey = "backpack"; private static readonly Dictionary _smell = new Dictionary(StringComparer.OrdinalIgnoreCase); private static readonly Dictionary _through = new Dictionary(StringComparer.OrdinalIgnoreCase); private static readonly Dictionary _containers = new Dictionary(StringComparer.OrdinalIgnoreCase); private static readonly Dictionary _overrides = new Dictionary(StringComparer.OrdinalIgnoreCase); private static readonly Dictionary _kind = new Dictionary(StringComparer.OrdinalIgnoreCase); private const int FileFormat = 4; private const int SMELL_REKEY_FORMAT = 3; private static bool _formatUpgraded; private static bool _readFailed; private static bool _loaded; private static bool _discovered; private static bool _everDiscovered; private static float _nextRescanAt; private static string _path; private const float RESCAN_INTERVAL = 60f; private const int PAYLOAD_VERSION = 1; private const int PAYLOAD_MAX_BYTES = 8192; private static bool _hasHost; private static readonly Dictionary _hostSmell = new Dictionary(StringComparer.OrdinalIgnoreCase); private static readonly Dictionary _hostThrough = new Dictionary(StringComparer.OrdinalIgnoreCase); private static readonly Dictionary _hostOverrides = new Dictionary(StringComparer.OrdinalIgnoreCase); private static float _hostDefaultProduct = 1f; private static float _hostDefaultOther = 0.5f; private static float _hostBagWeight = 1f; internal const string OtherKey = "Other"; private const float DefaultSmellProduct = 1f; private const float DefaultSmellOther = 0.5f; internal const string UnpackagedKey = "(none)"; internal static bool UsingHostValues => _hasHost; internal static bool HasDiscovered => _everDiscovered; internal static float BackpackWeight => Mathf.Clamp01(_hasHost ? _hostBagWeight : LocalBackpackWeight); internal static float LocalBackpackWeight { get { EnsureLoaded(); float value; return _containers.TryGetValue("backpack", out value) ? Mathf.Clamp01(value) : 1f; } } private static string Path { get { if (string.IsNullOrEmpty(_path)) { string path = System.IO.Path.Combine(MelonEnvironment.UserDataDirectory, "DropDaDeuce-K9Patrol"); _path = System.IO.Path.Combine(path, "K9Scents.cfg"); } return _path; } } internal static string BuildHostPayload() { if (!_everDiscovered) { return string.Empty; } EnsureLoaded(); CultureInfo invariantCulture = CultureInfo.InvariantCulture; StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append(1.ToString(invariantCulture)).Append('|').Append(LocalBackpackWeight.ToString("0.###", invariantCulture)) .Append('|') .Append(1f.ToString("0.###", invariantCulture)) .Append('|') .Append(0.5f.ToString("0.###", invariantCulture)); int dropped = 0; AppendSection(stringBuilder, 'S', _smell, invariantCulture, ref dropped); AppendSection(stringBuilder, 'P', _through, invariantCulture, ref dropped); AppendSection(stringBuilder, 'I', _overrides, invariantCulture, ref dropped); if (dropped > 0) { Logger.Warning($"K9 scent payload skipped {dropped} row(s) whose name contains '|' or '~'. " + "Those rows cannot cross the wire and will read as the host's default on every client."); } string text = stringBuilder.ToString(); if (Encoding.UTF8.GetByteCount(text) > 8192) { StringBuilder stringBuilder2 = new StringBuilder(); stringBuilder2.Append(1.ToString(invariantCulture)).Append('|').Append(LocalBackpackWeight.ToString("0.###", invariantCulture)) .Append('|') .Append(1f.ToString("0.###", invariantCulture)) .Append('|') .Append(0.5f.ToString("0.###", invariantCulture)); int dropped2 = 0; AppendSection(stringBuilder2, 'S', _smell, invariantCulture, ref dropped2); AppendSection(stringBuilder2, 'P', _through, invariantCulture, ref dropped2); stringBuilder2.Append("|I"); Logger.Warning($"K9 scent payload is too large with {_overrides.Count} [items] override(s); " + "publishing without them. Every overridden item will score on its KIND row for every player in this lobby, including you."); text = stringBuilder2.ToString(); if (Encoding.UTF8.GetByteCount(text) > 8192) { Logger.Warning("K9 scent table still will not fit in lobby data; publishing nothing. Every player, including you, scores from their own K9Scents.cfg this session."); return string.Empty; } } return text; } private static void AppendSection(StringBuilder sb, char tag, Dictionary rows, CultureInfo c, ref int dropped) { sb.Append('|').Append(tag); foreach (KeyValuePair item in Sorted(rows)) { if (item.Key.IndexOf('|') >= 0 || item.Key.IndexOf('~') >= 0 || item.Key.IndexOf('=') >= 0) { dropped++; } else { sb.Append('~').Append(item.Key).Append('=') .Append(item.Value.ToString("0.###", c)); } } } internal static void ApplyHostPayload(string payload) { if (string.IsNullOrEmpty(payload)) { return; } try { string[] array = payload.Split('|'); if (array.Length != 7) { Logger.Warning($"K9 scent payload has {array.Length} fields, expected 7 - ignoring, " + "staying on local values."); return; } CultureInfo invariantCulture = CultureInfo.InvariantCulture; if (!int.TryParse(array[0], NumberStyles.Integer, invariantCulture, out var result) || result != 1) { Logger.Warning($"K9 scent payload version '{array[0]}' is not {1} - ignoring, " + "staying on local values."); return; } if (!float.TryParse(array[1], NumberStyles.Float, invariantCulture, out var result2) || !float.TryParse(array[2], NumberStyles.Float, invariantCulture, out var result3) || !float.TryParse(array[3], NumberStyles.Float, invariantCulture, out var result4)) { Logger.Warning("K9 scent payload header did not parse - ignoring, staying on local values."); return; } Dictionary dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); Dictionary dictionary2 = new Dictionary(StringComparer.OrdinalIgnoreCase); Dictionary dictionary3 = new Dictionary(StringComparer.OrdinalIgnoreCase); if (!ParseSection(array[4], 'S', dictionary, invariantCulture) || !ParseSection(array[5], 'P', dictionary2, invariantCulture) || !ParseSection(array[6], 'I', dictionary3, invariantCulture)) { Logger.Warning("K9 scent payload body did not parse - ignoring, staying on local values."); return; } _hostSmell.Clear(); _hostThrough.Clear(); _hostOverrides.Clear(); foreach (KeyValuePair item in dictionary) { _hostSmell[item.Key] = Mathf.Clamp(item.Value, 0f, 100f); } foreach (KeyValuePair item2 in dictionary2) { _hostThrough[item2.Key] = Mathf.Clamp01(item2.Value); } foreach (KeyValuePair item3 in dictionary3) { _hostOverrides[item3.Key] = Mathf.Clamp(item3.Value, 0f, 100f); } _hostBagWeight = Mathf.Clamp01(result2); _hostDefaultProduct = Mathf.Clamp(result3, 0f, 100f); _hostDefaultOther = Mathf.Clamp(result4, 0f, 100f); _hasHost = true; Logger.Msg($"K9 scent table applied from host: {_hostSmell.Count} kind(s), " + $"{_hostThrough.Count} packaging, {_hostOverrides.Count} override(s), " + "backpack weight " + _hostBagWeight.ToString("0.###", invariantCulture) + ". Your own K9Scents.cfg is not being used in this lobby."); } catch (Exception ex) { Logger.Warning("K9 scent payload threw while parsing (" + ex.Message + ") - staying on local values."); } } private static bool ParseSection(string field, char tag, Dictionary into, CultureInfo c) { if (string.IsNullOrEmpty(field) || field[0] != tag) { return false; } if (field.Length == 1) { return true; } string[] array = field.Substring(1).Split('~'); foreach (string text in array) { if (text.Length != 0) { int num = text.IndexOf('='); if (num <= 0) { return false; } string key = text.Substring(0, num); if (!float.TryParse(text.Substring(num + 1), NumberStyles.Float, c, out var result)) { return false; } into[key] = result; } } return true; } internal static void ClearHostTable() { if (_hasHost) { _hasHost = false; _hostSmell.Clear(); _hostThrough.Clear(); _hostOverrides.Clear(); _hostDefaultProduct = 1f; _hostDefaultOther = 0.5f; _hostBagWeight = 1f; Logger.Msg("K9 scent table cleared - back to your own K9Scents.cfg."); } } internal static void Rearm() { _nextRescanAt = 0f; } internal static bool Reload() { _smell.Clear(); _through.Clear(); _overrides.Clear(); _containers.Clear(); _kind.Clear(); _loaded = false; _discovered = false; _readFailed = false; _nextRescanAt = 0f; Logger.Msg("K9 scent table dropped; K9Scents.cfg will be re-read on the next contraband scan."); return true; } private static string KindOf(ItemDefinition def, out bool isDrug) { //IL_002f: Unknown result type (might be due to invalid IL or missing references) isDrug = false; ProductDefinition val = K9Cast.AsProductDefinition(def); if ((Object)(object)val != (Object)null) { EDrugType? val2 = SafeDrugType(val); if (val2.HasValue) { isDrug = true; return DrugKey(val2.Value); } } return SafeCategoryKey(def); } private static string DrugKey(EDrugType drug) { //IL_0004: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Expected I4, but got Unknown //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Expected I4, but got Unknown if (1 == 0) { } string result = (int)drug switch { 0 => "Weed", 1 => "Meth", 2 => "Cocaine", 3 => "MDMA", 4 => "Shrooms", 5 => "Heroin", _ => $"DrugType{(int)drug}", }; if (1 == 0) { } return result; } private static string SafeCategoryKey(ItemDefinition def) { try { string text = ((object)Unsafe.As(ref ((BaseItemDefinition)def).Category)/*cast due to .constrained prefix*/).ToString(); return string.IsNullOrEmpty(text) ? "Other" : text; } catch { return "Other"; } } private static float DefaultThroughputFor(string packagingId, EStealthLevel level) { //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Expected I4, but got Unknown if (!string.IsNullOrEmpty(packagingId)) { switch (packagingId.ToLowerInvariant()) { case "baggie": return 0.6f; case "brick": return 0.6f; case "jar": return 0.4f; } } if (1 == 0) { } float result = (int)level switch { 0 => 0.6f, 1 => 0.4f, 2 => 0.25f, _ => 0.6f, }; if (1 == 0) { } return result; } internal static float SmellOf(string itemId, bool isProduct) { EnsureLoaded(); if (_hasHost) { if (!string.IsNullOrEmpty(itemId)) { if (_hostOverrides.TryGetValue(itemId, out var value)) { return Mathf.Max(0f, value); } if (_kind.TryGetValue(itemId, out var value2) && _hostSmell.TryGetValue(value2, out var value3)) { return Mathf.Max(0f, value3); } } return isProduct ? _hostDefaultProduct : _hostDefaultOther; } if (!string.IsNullOrEmpty(itemId)) { if (_overrides.TryGetValue(itemId, out var value4)) { return Mathf.Max(0f, value4); } if (_kind.TryGetValue(itemId, out var value5) && _smell.TryGetValue(value5, out var value6)) { return Mathf.Max(0f, value6); } } return isProduct ? 1f : 0.5f; } internal static string KindOfId(string itemId) { EnsureLoaded(); if (string.IsNullOrEmpty(itemId)) { return null; } if (_overrides.ContainsKey(itemId)) { return "[items] override"; } string value; return _kind.TryGetValue(itemId, out value) ? value : null; } internal static float PackagingMultiplier(string packagingId) { EnsureLoaded(); string text = (string.IsNullOrEmpty(packagingId) ? "(none)" : packagingId); if (_hasHost) { if (!_hostThrough.TryGetValue(text, out var value)) { value = ((text == "(none)") ? 1f : DefaultThroughputFor(text, (EStealthLevel)0)); } return Mathf.Clamp01(value); } if (!_through.TryGetValue(text, out var value2)) { value2 = ((text == "(none)") ? 1f : DefaultThroughputFor(text, (EStealthLevel)0)); } return Mathf.Clamp01(value2); } internal static float RadiusMultiplier(float score) { if (score <= 0f) { return 0f; } float num = Mathf.Max(0.01f, K9Config.ScentFullRadiusScore.Value); float num2 = Mathf.Max(0.1f, K9Config.ScentMaxRadiusMultiplier.Value); return Mathf.Min(score / num, num2); } internal static void DiscoverOnce() { //IL_00ef: Unknown result type (might be due to invalid IL or missing references) if (_discovered && Time.unscaledTime < _nextRescanAt) { return; } try { Registry instance = Singleton.Instance; if ((Object)(object)instance == (Object)null) { return; } List allItems = instance.GetAllItems(); if (allItems == null || allItems.Count == 0) { return; } _discovered = true; _nextRescanAt = Time.unscaledTime + 60f; EnsureLoaded(); int num = 0; int num2 = 0; foreach (ItemDefinition item in allItems) { if ((Object)(object)item == (Object)null) { continue; } PackagingDefinition val = K9Cast.AsPackaging(item); if ((Object)(object)val != (Object)null) { if (!_through.ContainsKey(((BaseItemDefinition)val).ID)) { _through[((BaseItemDefinition)val).ID] = DefaultThroughputFor(((BaseItemDefinition)val).ID, val.StealthLevel); num2++; } } else if (!((Object)(object)K9Cast.AsProductDefinition(item) == (Object)null) || SafeIsIllegal(item)) { bool isDrug; string text = KindOf(item, out isDrug); _kind[((BaseItemDefinition)item).ID] = text; if (!_smell.ContainsKey(text)) { _smell[text] = (isDrug ? 1f : 0.5f); num++; } } } if (!_through.ContainsKey("(none)")) { _through["(none)"] = 1f; num2++; } if (!_containers.ContainsKey("backpack")) { _containers["backpack"] = 1f; num2++; } if (num > 0 || num2 > 0) { Logger.Msg($"K9 scent table: {_smell.Count} kind(s) of contraband and {_through.Count} packaging " + $"type(s) known across {_kind.Count} item(s), {num} kind(s) and {num2} " + "packaging type(s) NEW this scan" + (_everDiscovered ? " (a mod or a game update added them mid-session)" : "") + ". Edit K9Scents.cfg to tune them."); } _everDiscovered = true; if (num > 0 || num2 > 0 || _formatUpgraded) { _formatUpgraded = false; Save(); } } catch (Exception ex) { Logger.Warning("K9 scent table discovery failed (" + ex.Message + "); defaults will be used until the next scan."); _discovered = true; _nextRescanAt = Time.unscaledTime + 60f; } } private static EDrugType? SafeDrugType(ProductDefinition p) { //IL_0020: Unknown result type (might be due to invalid IL or missing references) try { if (p.DrugTypes != null && p.DrugTypes.Count > 0) { return p.DrugType; } } catch { } return null; } private static bool SafeIsIllegal(ItemDefinition def) { //IL_0003: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Invalid comparison between Unknown and I4 try { return (int)((BaseItemDefinition)def).legalStatus > 0; } catch { return false; } } private static void EnsureLoaded() { if (_loaded) { return; } _loaded = true; try { if (!File.Exists(Path)) { return; } string[] array = File.ReadAllLines(Path); int num = ReadFormat(array); if (num < 4) { _formatUpgraded = true; } bool flag = num < 2; bool flag2 = num < 3; if (flag) { Logger.Warning("K9 K9Scents.cfg is in the original format and is being regenerated. Packaging used to be a PERCENTAGE HIDDEN and is now the fraction of the smell that GETS THROUGH - the two point opposite ways, so the old numbers cannot be carried over safely. Any tuning you had in this file is gone; the new file is written on the next scan."); return; } if (flag2) { Logger.Warning("K9 K9Scents.cfg lists a smell per ITEM and now lists one per KIND - 'Weed' rather than 'ogkush', 'sourdiesel' and two more; 'Ingredient' rather than twelve raw materials. Your [packaging] numbers are KEPT; the [smell] section is rewritten from defaults on the next scan, because an item's value cannot be promoted to its kind without applying it to everything else of that kind too. To tune one item again, put it under [items]."); } string text = string.Empty; string[] array2 = array; foreach (string text2 in array2) { string text3 = text2.Trim(); if (text3.Length == 0 || text3[0] == '#') { continue; } if (text3[0] == '[') { text = text3.ToLowerInvariant(); continue; } int num2 = text3.IndexOf('='); if (num2 <= 0) { continue; } string key = text3.Substring(0, num2).Trim(); string text4 = text3.Substring(num2 + 1).Trim(); int num3 = text4.IndexOf('#'); if (num3 >= 0) { text4 = text4.Substring(0, num3).Trim(); } if (!float.TryParse(text4, NumberStyles.Float, CultureInfo.InvariantCulture, out var result)) { continue; } switch (text) { case "[containers]": _containers[key] = result; break; case "[packaging]": _through[key] = result; break; case "[items]": _overrides[key] = result; break; case "[smell]": if (!flag2) { _smell[key] = result; } break; } } Logger.Debug($"K9 scent table loaded from {Path}: {_smell.Count} kind row(s), " + $"{_through.Count} packaging row(s), {_overrides.Count} per-item override(s)."); } catch (Exception ex) { _readFailed = true; Logger.Warning("K9 could not read K9Scents.cfg (" + ex.Message + "). Default smells will be used for this session and THE FILE WILL NOT BE WRITTEN, so whatever you have tuned in it is safe. Close anything that has it open and restart the game to pick your numbers back up."); } } private static int ReadFormat(string[] lines) { foreach (string text in lines) { string text2 = text.Trim(); if (text2.Length == 0) { continue; } if (text2[0] != '#') { break; } string text3 = text2.TrimStart('#').Trim(); if (text3.StartsWith("format", StringComparison.OrdinalIgnoreCase)) { int num = text3.IndexOf('='); if (num >= 0 && int.TryParse(text3.Substring(num + 1).Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) { return result; } } } return 1; } private static void Save() { if (_readFailed) { Logger.Debug("K9 not writing K9Scents.cfg: it could not be read this session, so the table in memory is defaults rather than yours, and saving it would overwrite your numbers."); return; } try { string directoryName = System.IO.Path.GetDirectoryName(Path); if (!string.IsNullOrEmpty(directoryName)) { Directory.CreateDirectory(directoryName); } StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("# K9 Patrol - scent table"); stringBuilder.AppendLine($"# format = {4}"); stringBuilder.AppendLine("#"); stringBuilder.AppendLine("# This file is GENERATED and then MAINTAINED by the mod. Every KIND of contraband the"); stringBuilder.AppendLine("# game knows about gets a row the first time it is seen; your edits are kept and only"); stringBuilder.AppendLine("# genuinely new kinds are appended. Delete the file to start over from defaults."); stringBuilder.AppendLine("#"); stringBuilder.AppendLine("# HOW A DOG DECIDES IT CAN SMELL YOU"); stringBuilder.AppendLine("#"); stringBuilder.AppendLine("# for each item in your hotbar:"); stringBuilder.AppendLine("# units = how many you are holding x how many fit in its packaging"); stringBuilder.AppendLine("# score += smell x units x packaging"); stringBuilder.AppendLine("#"); stringBuilder.AppendLine("# sniff radius = SniffRadius x (score / ScentFullRadiusScore), capped at"); stringBuilder.AppendLine("# ScentMaxRadiusMultiplier. Those two live in K9Patrol.cfg."); stringBuilder.AppendLine("#"); stringBuilder.AppendLine("# A wall between you and the dog cuts whatever is left again - that one is not"); stringBuilder.AppendLine("# tunable here."); stringBuilder.AppendLine("#"); stringBuilder.AppendLine("# BOTH COLUMNS POINT THE SAME WAY: BIGGER = EASIER TO SMELL."); stringBuilder.AppendLine("#"); stringBuilder.AppendLine("# IN MULTIPLAYER THE HOST'S COPY OF THIS FILE IS USED FOR EVERYBODY. Yours applies"); stringBuilder.AppendLine("# when you are hosting or playing alone. If you edit this while playing on someone"); stringBuilder.AppendLine("# else's game, nothing will change - that is not a bug. The same goes for the"); stringBuilder.AppendLine("# [containers] section below."); stringBuilder.AppendLine(); stringBuilder.AppendLine("[smell]"); stringBuilder.AppendLine("# Smell PER UNIT, by KIND of contraband rather than by item, so one number covers"); stringBuilder.AppendLine("# every strain of weed and one covers all the raw materials. 1 is a normal drug."); stringBuilder.AppendLine("# Bigger = the dog notices it from further away; 0 means it cannot smell that kind"); stringBuilder.AppendLine("# at all. The mod asks the GAME what kind each item is - a finished drug reports its"); stringBuilder.AppendLine("# drug type, everything else reports its item category - so a drug added by a mod"); stringBuilder.AppendLine("# lands on the right row here on its own, and a new kind appears as a new row."); foreach (KeyValuePair item in Sorted(_smell)) { stringBuilder.AppendLine(item.Key + " = " + item.Value.ToString("0.###", CultureInfo.InvariantCulture)); } stringBuilder.AppendLine(); stringBuilder.AppendLine("[packaging]"); stringBuilder.AppendLine("# HOW MUCH OF THE SMELL GETS THROUGH. 1 lets all of it out, 0.4 lets 40% out, 0"); stringBuilder.AppendLine("# would be a perfect cloak (allowed - but then everybody uses that packaging and the"); stringBuilder.AppendLine("# dog is decoration)."); stringBuilder.AppendLine("#"); stringBuilder.AppendLine("# Packaging holds MORE than one unit, and the units count. A jar hides each unit"); stringBuilder.AppendLine("# better AND holds several, so the two pull against each other - which is why packing"); stringBuilder.AppendLine("# up is a real decision rather than always the safe move."); stringBuilder.AppendLine("#"); stringBuilder.AppendLine("# '(none)' is loose product with nothing around it."); foreach (KeyValuePair item2 in Sorted(_through)) { stringBuilder.AppendLine(item2.Key + " = " + item2.Value.ToString("0.###", CultureInfo.InvariantCulture)); } stringBuilder.AppendLine(); stringBuilder.AppendLine("[containers]"); stringBuilder.AppendLine("# HOW MUCH SMELL GETS OUT OF A BAG, if you use a mod that gives you one. Same column"); stringBuilder.AppendLine("# as [packaging] above: 1 means the bag hides nothing and a stash inside it smells the"); stringBuilder.AppendLine("# same as one in your hands, 0.5 lets half out, 0 means the dog cannot smell into a bag"); stringBuilder.AppendLine("# at all. Packaging still applies on top, so a jar in a bag is hidden twice over."); stringBuilder.AppendLine("# THIS DOES NOTHING IN AN UNMODDED GAME - the base game has no bag to sniff."); foreach (KeyValuePair item3 in Sorted(_containers)) { stringBuilder.AppendLine(item3.Key + " = " + item3.Value.ToString("0.###", CultureInfo.InvariantCulture)); } stringBuilder.AppendLine(); stringBuilder.AppendLine("[items]"); stringBuilder.AppendLine("# OPTIONAL, AND YOURS - THE MOD NEVER WRITES A ROW HERE."); stringBuilder.AppendLine("#"); stringBuilder.AppendLine("# A row is one exact item ID and it beats that item's kind above. Use it when you"); stringBuilder.AppendLine("# want one thing to differ from everything else like it - a signature strain the dog"); stringBuilder.AppendLine("# is trained on, a raw material you think should be quieter, or a modded drug that"); stringBuilder.AppendLine("# lands on a kind you disagree with. Same scale as [smell]: 1 is a normal drug."); stringBuilder.AppendLine("#"); stringBuilder.AppendLine("# ogkush = 2"); stringBuilder.AppendLine("# sporesyringe = 0"); foreach (KeyValuePair item4 in Sorted(_overrides)) { stringBuilder.AppendLine(item4.Key + " = " + item4.Value.ToString("0.###", CultureInfo.InvariantCulture)); } string text = Path + ".tmp"; File.WriteAllText(text, stringBuilder.ToString()); if (File.Exists(Path)) { File.Replace(text, Path, null); } else { File.Move(text, Path); } Logger.Msg("K9 scent table written to " + Path + "."); } catch (Exception ex) { try { string path = Path + ".tmp"; if (File.Exists(path)) { File.Delete(path); } } catch { } Logger.Warning("K9 could not write K9Scents.cfg (" + ex.Message + "). Tuning will not persist, but detection works normally this session."); } } private static List> Sorted(Dictionary d) { List> list = new List>(d); list.Sort((KeyValuePair a, KeyValuePair b) => string.Compare(a.Key, b.Key, StringComparison.OrdinalIgnoreCase)); return list; } } internal static class K9Cast { internal static ProductItemInstance AsProduct(ItemInstance item) { if (item == null) { return null; } return (ProductItemInstance)(object)((item is ProductItemInstance) ? item : null); } internal static ProductDefinition AsProductDefinition(ItemDefinition definition) { if ((Object)(object)definition == (Object)null) { return null; } return (ProductDefinition)(object)((definition is ProductDefinition) ? definition : null); } internal static PackagingDefinition AsPackaging(ItemDefinition definition) { if ((Object)(object)definition == (Object)null) { return null; } return (PackagingDefinition)(object)((definition is PackagingDefinition) ? definition : null); } internal static PoliceOfficer AsOfficer(NPC npc) { if ((Object)(object)npc == (Object)null) { return null; } return (PoliceOfficer)(object)((npc is PoliceOfficer) ? npc : null); } internal static SkinnedMeshRenderer AsSkinnedMesh(Renderer renderer) { if ((Object)(object)renderer == (Object)null) { return null; } return (SkinnedMeshRenderer)(object)((renderer is SkinnedMeshRenderer) ? renderer : null); } } } namespace K9_Patrol.Source.Networking { public static class K9NetSync { public enum K9NetState : byte { Idle, Following, Tracking, Investigating, Searching } private readonly struct UnitState { internal readonly string OfficerGuid; internal readonly K9NetState State; internal readonly string TargetPlayerCode; internal UnitState(string guid, K9NetState state, string target) { OfficerGuid = guid; State = state; TargetPlayerCode = target ?? string.Empty; } } private static readonly Dictionary _serverState = new Dictionary(); private static readonly Dictionary _clientDogs = new Dictionary(); private static bool _wired; private static float _nextSnapshotAt; private static float _nextScentCheckAt; private static string _lastScentPayload = string.Empty; private static float _nextBeaconCheckAt; private static float _nextRetryAt; private static float _lobbySettledAt; private static bool _beaconFaultReported; private static bool _wasInLobby; private static bool _hostKeysPublished; private const float SNAPSHOT_INTERVAL = 3f; private const float SCENT_CHECK_INTERVAL = 5f; private const float BEACON_CHECK_INTERVAL = 5f; private const float RETRY_INTERVAL = 15f; private const float BEACON_GRACE = 15f; private static bool _pumpAnnounced; private static readonly HashSet _guidFallbackWarned = new HashSet(); private static readonly HashSet _spawnMissWarned = new HashSet(); public static bool IsServer => K9Net.IsServer(); public static void InitializeIfNeeded() { if (!_wired) { _wired = true; K9SteamLink.OnContraband = HandleContrabandFromClient; K9SteamLink.OnUnit = HandleUnitFromHost; K9SteamLink.OnSnapshot = HandleSnapshotFromHost; K9SteamLink.OnEvent = HandleEventFromPeer; K9SteamLink.TryInit(); } } public static void ServerSpawn(PoliceOfficer officer) { if (!IsServer || (Object)(object)officer == (Object)null) { return; } int num = OfficerId(officer); if (num == 0) { Logger.Debug("K9 ServerSpawn: officer has no usable network id yet."); return; } string text = OfficerGuid(officer); if (!_serverState.ContainsKey(num)) { _serverState[num] = new UnitState(text, K9NetState.Following, string.Empty); } K9SteamLink.BroadcastUnit(1, num, text, 1, string.Empty); } public static void ServerState(PoliceOfficer officer, K9NetState state, Player target) { if (!IsServer || (Object)(object)officer == (Object)null) { return; } int num = OfficerId(officer); if (num != 0) { string text = OfficerGuid(officer); string text2 = (((Object)(object)target != (Object)null) ? K9ContrabandRegistry.PlayerCodeOf(target) : string.Empty); if (!_serverState.TryGetValue(num, out var value) || value.State != state || !(value.TargetPlayerCode == text2)) { _serverState[num] = new UnitState(text, state, text2); K9SteamLink.BroadcastUnit(2, num, text, (byte)state, text2); } } } public static void ServerDespawn(PoliceOfficer officer) { if (IsServer && !((Object)(object)officer == (Object)null)) { int num = OfficerId(officer); if (num != 0) { _serverState.Remove(num); K9SteamLink.BroadcastUnit(3, num, OfficerGuid(officer), 0, string.Empty); } } } public static void ServerEnsureRegistered(PoliceOfficer officer) { if (IsServer && !((Object)(object)officer == (Object)null)) { int num = OfficerId(officer); if (num != 0 && !_serverState.ContainsKey(num)) { _serverState[num] = new UnitState(OfficerGuid(officer), K9NetState.Following, string.Empty); Logger.Debug($"K9 registered officer {num} late (network id was not ready when its unit spawned)."); } } } internal static void OnMemberJoinedServerSide() { if (IsServer) { K9SteamLink.PublishHostKeys(K9DogTuning.BuildHostPayload(), K9ScentTable.BuildHostPayload()); SendSnapshot(); _lobbySettledAt = Time.unscaledTime; _beaconFaultReported = false; } } private static void SendSnapshot() { List list = new List(_serverState.Count); foreach (KeyValuePair item in _serverState) { list.Add(new K9SnapshotEntry { OfficerId = item.Key, OfficerGuid = item.Value.OfficerGuid, State = (byte)item.Value.State, TargetPlayerCode = item.Value.TargetPlayerCode }); } K9SteamLink.BroadcastSnapshot(list); } private static void HandleContrabandFromClient(string playerCode, uint seq, byte flags, byte minStealth, float scent) { bool carrying = (flags & 1) != 0; K9ContrabandRegistry.Submit(playerCode, seq, carrying, minStealth, scent); } private static void CheckBeacons() { if (!K9SteamLink.IsInLobby) { _lobbySettledAt = 0f; _beaconFaultReported = false; } else if (_lobbySettledAt <= 0f) { _lobbySettledAt = Time.unscaledTime; } else if (!(Time.unscaledTime - _lobbySettledAt < 15f)) { List membersWithoutMod = K9SteamLink.GetMembersWithoutMod(); if (membersWithoutMod.Count == 0) { _beaconFaultReported = false; } else if (!_beaconFaultReported) { _beaconFaultReported = true; Logger.Warning(string.Format("K9 PATROL IS NOT RESPONDING ON {0} MACHINE(S) IN THIS LOBBY: {1}. ", membersWithoutMod.Count, string.Join(", ", membersWithoutMod)) + "K9 Patrol is built on the assumption that everyone in the lobby has it installed, and these players will never be detected by a police dog. Check that they have the mod, that it matches their game build (Mono vs IL2CPP), and that they also have SteamNetworkLib."); } } } private static void HandleUnitFromHost(byte op, int officerId, string officerGuid, byte state, string targetPlayerCode) { switch ((K9UnitMessage.Op)op) { case K9UnitMessage.Op.Spawn: ClientSpawn(officerId, officerGuid); ClientApplyState(officerId, state, targetPlayerCode); break; case K9UnitMessage.Op.State: ClientSpawn(officerId, officerGuid); ClientApplyState(officerId, state, targetPlayerCode); break; case K9UnitMessage.Op.Despawn: ClientDespawn(officerId); break; } } private static void HandleSnapshotFromHost(List entries) { HashSet hashSet = new HashSet(); foreach (K9SnapshotEntry entry in entries) { hashSet.Add(entry.OfficerId); ClientSpawn(entry.OfficerId, entry.OfficerGuid); ClientApplyState(entry.OfficerId, entry.State, entry.TargetPlayerCode); } List list = null; foreach (int key in _clientDogs.Keys) { if (!hashSet.Contains(key)) { (list ?? (list = new List())).Add(key); } } if (list == null) { return; } foreach (int item in list) { Logger.Debug($"K9 snapshot despawn: officer {item} is no longer in the host's list."); ClientDespawn(item); } } private static void ClientSpawn(int officerId, string officerGuid) { if (IsServer || (_clientDogs.TryGetValue(officerId, out var value) && (Object)(object)value != (Object)null)) { return; } PoliceOfficer val = FindOfficer(officerId, officerGuid); if ((Object)(object)val == (Object)null) { if (_spawnMissWarned.Add(officerId)) { Logger.Debug($"K9 client: host reports a unit on officer {officerId} (guid {officerGuid}) " + "which is not in this scene yet. Will retry on the next snapshot."); } return; } _spawnMissWarned.Remove(officerId); K9NPC k9NPC = K9DogFactory.Build(val, $"K9_ClientDog_{officerId}"); if (!((Object)(object)k9NPC == (Object)null)) { _clientDogs[officerId] = k9NPC; Logger.Msg($"K9 client dog spawned for officer {officerId} ({((Object)val).name})."); } } private static void ClientApplyState(int officerId, byte state, string targetPlayerCode) { if (IsServer || !_clientDogs.TryGetValue(officerId, out var value) || (Object)(object)value == (Object)null) { return; } Player val = null; if ((state == 2 || state == 3 || state == 4) && !string.IsNullOrEmpty(targetPlayerCode)) { val = FindPlayer(targetPlayerCode); } bool flag = (Object)(object)value._pursuitTarget != (Object)null; value._pursuitTarget = val; if (flag || !((Object)(object)val != (Object)null)) { return; } try { value.TriggerAlert(); } catch { } } private static void ClientDespawn(int officerId) { if (_clientDogs.TryGetValue(officerId, out var value) && (Object)(object)value != (Object)null && (Object)(object)((Component)value).gameObject != (Object)null) { Object.Destroy((Object)(object)((Component)value).gameObject); } _clientDogs.Remove(officerId); } internal static void ServerClearAll() { _serverState.Clear(); _guidFallbackWarned.Clear(); } internal static void ClientClearAll() { foreach (KeyValuePair clientDog in _clientDogs) { if ((Object)(object)clientDog.Value != (Object)null && (Object)(object)((Component)clientDog.Value).gameObject != (Object)null) { Object.Destroy((Object)(object)((Component)clientDog.Value).gameObject); } } _clientDogs.Clear(); _spawnMissWarned.Clear(); } public static void Pump() { if (!_pumpAnnounced) { _pumpAnnounced = true; Logger.Msg("K9 pump is running (first tick)."); } K9SteamLink.Pump(); K9ContrabandPublisher.Tick(); } public static void PostPump() { float unscaledTime = Time.unscaledTime; if (!K9SteamLink.Available && unscaledTime >= _nextRetryAt) { _nextRetryAt = unscaledTime + 15f; K9SteamLink.Retry(); if (K9SteamLink.Available) { InitializeIfNeeded(); } } bool isInLobby = K9SteamLink.IsInLobby; if (isInLobby != _wasInLobby) { _wasInLobby = isInLobby; _hostKeysPublished = false; _lastScentPayload = string.Empty; _nextScentCheckAt = 0f; _lobbySettledAt = (isInLobby ? unscaledTime : 0f); _beaconFaultReported = false; if (!isInLobby) { ClientClearAll(); K9ContrabandRegistry.Clear(); K9ContrabandPublisher.Reset(); Logger.Debug("K9 left the lobby; client dogs and contraband reports dropped."); } } if (!isInLobby || !IsServer) { return; } if (!_hostKeysPublished) { _hostKeysPublished = true; K9SteamLink.PublishHostKeys(K9DogTuning.BuildHostPayload(), K9ScentTable.BuildHostPayload()); } if (unscaledTime >= _nextSnapshotAt) { _nextSnapshotAt = unscaledTime + 3f; PruneDeadOfficers(); SendSnapshot(); } if (unscaledTime >= _nextScentCheckAt) { _nextScentCheckAt = unscaledTime + 5f; string text = K9ScentTable.BuildHostPayload(); if (text != _lastScentPayload) { bool flag = string.IsNullOrEmpty(_lastScentPayload); _lastScentPayload = text; K9SteamLink.PublishHostKeys(K9DogTuning.BuildHostPayload(), text); Logger.Debug(flag ? "K9 scent table published for the first time (discovery has run)." : "K9 scent table changed; republished for every client in the lobby."); } } if (unscaledTime >= _nextBeaconCheckAt) { _nextBeaconCheckAt = unscaledTime + 5f; CheckBeacons(); } } private static void PruneDeadOfficers() { List list = null; foreach (KeyValuePair item in _serverState) { PoliceOfficer val = FindOfficer(item.Key, item.Value.OfficerGuid); if ((Object)(object)val == (Object)null || (Object)(object)((NPC)val).Health == (Object)null || ((NPC)val).Health.IsDead) { (list ?? (list = new List())).Add(item.Key); } } if (list == null) { return; } foreach (int item2 in list) { string officerGuid = _serverState[item2].OfficerGuid; _serverState.Remove(item2); K9SteamLink.BroadcastUnit(3, item2, officerGuid, 0, string.Empty); } } internal static void BroadcastDogYelp(PoliceOfficer officer) { if (!((Object)(object)officer == (Object)null)) { int num = OfficerId(officer); string text = OfficerGuid(officer); if (num == 0 && string.IsNullOrEmpty(text)) { Logger.Debug("K9 yelp not broadcast: the officer has neither a network id nor a GUID yet."); } else { K9SteamLink.BroadcastEvent(1, num, text); } } } private static void HandleEventFromPeer(byte kind, int officerId, string officerGuid) { if (kind == 1) { K9NPC k9NPC = FindLocalDog(officerId, officerGuid); if ((Object)(object)k9NPC == (Object)null) { Logger.Debug($"K9 yelp for officer {officerId} arrived but no dog here follows that officer."); } else { k9NPC.PlayRemoteYelp(); } } } private static K9NPC FindLocalDog(int officerId, string officerGuid) { try { foreach (K9NPC item in K9NPC.Live) { if ((Object)(object)item == (Object)null || (Object)(object)item._officer == (Object)null || officerId == 0 || OfficerId(item._officer) != officerId) { continue; } return item; } if (string.IsNullOrEmpty(officerGuid)) { return null; } foreach (K9NPC item2 in K9NPC.Live) { if ((Object)(object)item2 == (Object)null || (Object)(object)item2._officer == (Object)null || !(OfficerGuid(item2._officer) == officerGuid)) { continue; } return item2; } } catch { } return null; } internal static int OfficerId(PoliceOfficer officer) { if ((Object)(object)officer == (Object)null) { return 0; } try { NetworkObject networkObject = ((NetworkBehaviour)officer).NetworkObject; return ((Object)(object)networkObject != (Object)null) ? networkObject.ObjectId : 0; } catch { return 0; } } internal static string OfficerGuid(PoliceOfficer officer) { if ((Object)(object)officer == (Object)null) { return string.Empty; } try { return ((NPC)officer).GUID.ToString(); } catch { return string.Empty; } } private static PoliceOfficer FindOfficer(int objectId, string guid) { try { foreach (PoliceOfficer officer in PoliceOfficer.Officers) { if ((Object)(object)officer == (Object)null || OfficerId(officer) != objectId || objectId == 0) { continue; } return officer; } if (!string.IsNullOrEmpty(guid)) { foreach (PoliceOfficer officer2 in PoliceOfficer.Officers) { if ((Object)(object)officer2 == (Object)null || !(OfficerGuid(officer2) == guid)) { continue; } if (_guidFallbackWarned.Add(guid)) { if (IsServer) { Logger.Warning($"K9 resolved officer {guid} by GUID because ObjectId {objectId} is not in this scene. " + "This id came from THIS machine's own unit table, so it is stale local state, not a cross-machine disagreement - the table should have been cleared on the scene change."); } else { Logger.Warning($"K9 resolved officer {guid} by GUID because ObjectId {objectId} did not match anything here. " + "This id came off the wire, so NetworkObject.ObjectId IS NOT CONSISTENT ACROSS MACHINES and the design's section 2.2 is wrong - please report this line, it settles a question no session could test."); } } return officer2; } } } catch { } return null; } private static Player FindPlayer(string playerCode) { if (string.IsNullOrEmpty(playerCode)) { return null; } try { foreach (Player item in K9Manager.PlayerCache) { if ((Object)(object)item != (Object)null && K9ContrabandRegistry.PlayerCodeOf(item) == playerCode) { return item; } } } catch { } return null; } } internal static class K9SteamLink { private static IK9SteamLink _impl; private static bool _triedInit; internal static Action OnContraband; internal static Action OnUnit; internal static Action> OnSnapshot; internal static Action OnEvent; internal const float RETRY_INTERVAL_HINT = 15f; private static int _failedRetries; private static bool _escalated; internal static bool Available => _impl != null; internal static bool IsInLobby => _impl != null && _impl.IsInLobby; internal static string LocalPlayerCode => _impl?.LocalPlayerCode ?? string.Empty; internal static bool TryInit() { if (_impl != null) { return true; } if (_triedInit) { return false; } _triedInit = true; try { IK9SteamLink iK9SteamLink = Create(); if (iK9SteamLink != null && iK9SteamLink.Init()) { _impl = iK9SteamLink; Logger.Msg("K9 Steam link up."); return true; } Logger.Debug("K9 Steam link could not initialise yet (Steam not ready). Will retry."); } catch (Exception ex) { if (ex is TypeLoadException || ex is FileNotFoundException || ex is FileLoadException || ex is BadImageFormatException) { Logger.Warning("K9 could not load SteamNetworkLib (" + ex.GetType().Name + ": " + ex.Message + "). Multiplayer sync is off for this session. Install the package that MATCHES YOUR BUILD - ifBars-SteamNetworkLib_Mono for the Mono game, ifBars-SteamNetworkLib_Il2Cpp for the IL2CPP one. Single-player is completely unaffected."); } else { Logger.Debug($"K9 Steam link not ready yet ({ex.GetType().Name}: {ex.Message}). Retrying every {15}s."); } } return false; } [MethodImpl(MethodImplOptions.NoInlining)] private static IK9SteamLink Create() { return new K9SteamLinkImpl(); } internal static void Retry() { if (_impl != null) { return; } _triedInit = false; if (TryInit()) { _failedRetries = 0; return; } _failedRetries++; if (_failedRetries == 4 && !_escalated) { _escalated = true; Logger.Warning("K9 has been unable to reach Steam for about a minute. Multiplayer sync is off for this session; single-player is unaffected. If you are in a lobby and expecting K9 to work, this is the reason."); } } internal static void Pump() { try { _impl?.Pump(); } catch (Exception ex) { Logger.Debug("K9 Steam pump: " + ex.Message); } } internal static void Shutdown() { try { _impl?.Dispose(); } catch { } _impl = null; } internal static void PublishHostKeys(string tuningPayload, string scentsPayload) { try { _impl?.PublishHostKeys(tuningPayload, scentsPayload); } catch (Exception ex) { Logger.Warning("K9 could not publish host keys: " + ex.Message); } } internal static void PublishModBeacon() { try { _impl?.PublishModBeacon(); } catch (Exception ex) { Logger.Warning("K9 could not publish its mod beacon: " + ex.Message); } } internal static List GetMembersWithoutMod() { try { return _impl?.GetMembersWithoutMod() ?? new List(); } catch { return new List(); } } internal static void SendContrabandToHost(uint seq, byte flags, byte minStealth, float scent) { try { _impl?.SendContrabandToHost(seq, flags, minStealth, scent); } catch (Exception ex) { Logger.Debug("K9 contraband send failed: " + ex.Message); } } internal static void BroadcastUnit(byte op, int officerId, string officerGuid, byte state, string targetPlayerCode) { try { _impl?.BroadcastUnit(op, officerId, officerGuid, state, targetPlayerCode); } catch (Exception ex) { Logger.Debug("K9 unit broadcast failed: " + ex.Message); } } internal static void BroadcastSnapshot(List entries) { try { _impl?.BroadcastSnapshot(entries); } catch (Exception ex) { Logger.Debug("K9 snapshot broadcast failed: " + ex.Message); } } internal static void BroadcastEvent(byte kind, int officerId, string officerGuid) { try { _impl?.BroadcastEvent(kind, officerId, officerGuid); } catch (Exception ex) { Logger.Debug("K9 event broadcast failed: " + ex.Message); } } internal static string SteamIdToPlayerCode(ulong steamId) { return steamId.ToString(K9Proto.Inv); } } internal interface IK9SteamLink { bool IsInLobby { get; } string LocalPlayerCode { get; } bool Init(); void Pump(); void Dispose(); void PublishHostKeys(string tuningPayload, string scentsPayload); void PublishModBeacon(); List GetMembersWithoutMod(); void SendContrabandToHost(uint seq, byte flags, byte minStealth, float scent); void BroadcastUnit(byte op, int officerId, string officerGuid, byte state, string targetPlayerCode); void BroadcastSnapshot(List entries); void BroadcastEvent(byte kind, int officerId, string officerGuid); } internal struct K9SnapshotEntry { internal int OfficerId; internal string OfficerGuid; internal byte State; internal string TargetPlayerCode; } internal sealed class K9SteamLinkImpl : IK9SteamLink { private const int CHANNEL = 77; private const string KEY_HOST = "k9p.host"; private const string KEY_PROTO = "k9p.proto"; private const string KEY_CFG = "k9p.cfg"; private const string KEY_SCENTS = "k9p.scents"; private const string KEY_MOD = "k9p.mod"; private SteamNetworkClient _client; private string _localPlayerCode = string.Empty; private string _lastTuningSeen = string.Empty; private string _lastScentsSeen = string.Empty; private readonly HashSet _pumpFaultsReported = new HashSet(); private bool _firstPacketLogged; private const uint MAX_PACKET_BYTES = 524288u; private const int MAX_PACKETS_PER_PUMP = 64; private readonly HashSet _impostorWarned = new HashSet(); private readonly HashSet _warned = new HashSet(); public bool IsInLobby => _client != null && _client.IsInLobby; public string LocalPlayerCode => _localPlayerCode; public bool Init() { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Expected O, but got Unknown //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Expected O, but got Unknown NetworkRules val = new NetworkRules { MinReceiveChannel = 77, MaxReceiveChannel = 77, DefaultSendType = (EP2PSend)2 }; _client = new SteamNetworkClient(val); if (!_client.Initialize()) { _client = null; return false; } try { _localPlayerCode = _client.LocalPlayerId64.ToString(K9Proto.Inv); } catch { } _client.OnLobbyJoined += HandleLobbyJoined; _client.OnLobbyCreated += HandleLobbyCreated; _client.OnLobbyLeft += HandleLobbyLeft; _client.OnLobbyDataChanged += HandleLobbyDataChanged; _client.OnMemberJoined += HandleMemberJoined; _client.RegisterMessageHandler((Action)HandleContraband); _client.RegisterMessageHandler((Action)HandleUnit); _client.RegisterMessageHandler((Action)HandleSnapshot); _client.RegisterMessageHandler((Action)HandleEvent); Logger.Msg($"K9 Steam client initialised on channel {77}. localPlayerCode={_localPlayerCode} " + $"inLobby={_client.IsInLobby} steamSaysHost={SafeIsHost()} fishNetSaysServer={K9Net.IsServer()}"); if (_client.IsInLobby) { OnJoinedAnyLobby(); } return true; } public void Pump() { if (_client != null) { try { _client.ProcessIncomingMessages(); } catch (Exception ex) { PumpFault("ProcessIncomingMessages", ex); } DrainOurChannel(); } } private void DrainOurChannel() { //IL_00e5: Unknown result type (might be due to invalid IL or missing references) //IL_00bb: Unknown result type (might be due to invalid IL or missing references) try { uint num = default(uint); uint num2 = default(uint); CSteamID val = default(CSteamID); for (int i = 0; i < 64; i++) { if (!SteamNetworking.IsP2PPacketAvailable(ref num, 77)) { break; } if (num == 0 || num > 524288) { ReadAndDiscard(num); continue; } byte[] array = new byte[num]; if (!SteamNetworking.ReadP2PPacket(array, num, ref num2, ref val, 77)) { break; } if (num2 != 0) { byte[] array2; if (num2 == num) { array2 = array; } else { array2 = new byte[num2]; Array.Copy(array, array2, (int)num2); } if (!_firstPacketLogged) { _firstPacketLogged = true; Logger.Msg($"K9 first P2P packet received on channel {77} from {val.m_SteamID} ({num2} bytes). " + "The wire is live in this direction."); } Dispatch(array2, val); } } } catch (Exception ex) { PumpFault("DrainOurChannel", ex); } } private void Dispatch(byte[] payload, CSteamID sender) { //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_007c: Unknown result type (might be due to invalid IL or missing references) //IL_009d: Unknown result type (might be due to invalid IL or missing references) //IL_00be: Unknown result type (might be due to invalid IL or missing references) string messageType; try { if (!MessageSerializer.IsValidMessage(payload)) { return; } messageType = MessageSerializer.GetMessageType(payload); } catch (Exception ex) { PumpFault("Dispatch/header", ex); return; } if (string.IsNullOrEmpty(messageType)) { return; } try { switch (messageType) { case "k9p.contraband": HandleContraband(MessageSerializer.CreateMessage(payload), sender); break; case "k9p.unit": HandleUnit(MessageSerializer.CreateMessage(payload), sender); break; case "k9p.snapshot": HandleSnapshot(MessageSerializer.CreateMessage(payload), sender); break; case "k9p.event": HandleEvent(MessageSerializer.CreateMessage(payload), sender); break; } } catch (Exception ex2) { PumpFault("Dispatch/" + messageType, ex2); } } private void ReadAndDiscard(uint size) { try { uint num = ((size == 0) ? 1u : ((size > 524288) ? 524288u : size)); uint num2 = default(uint); CSteamID val = default(CSteamID); SteamNetworking.ReadP2PPacket(new byte[num], num, ref num2, ref val, 77); } catch { } } private void PumpFault(string where, Exception ex) { if (_pumpFaultsReported.Add(where)) { Logger.Error("K9 pump fault in " + where + ": " + ex.GetType().Name + ": " + ex.Message + ". Further faults here will be silent."); } } public void Dispose() { if (_client != null) { try { _client.OnLobbyJoined -= HandleLobbyJoined; _client.OnLobbyCreated -= HandleLobbyCreated; _client.OnLobbyLeft -= HandleLobbyLeft; _client.OnLobbyDataChanged -= HandleLobbyDataChanged; _client.OnMemberJoined -= HandleMemberJoined; } catch { } try { _client.Dispose(); } catch { } _client = null; } } private void HandleLobbyJoined(object s, LobbyJoinedEventArgs e) { OnJoinedAnyLobby(); } private void HandleLobbyCreated(object s, LobbyCreatedEventArgs e) { OnJoinedAnyLobby(); } private void HandleLobbyLeft(object s, LobbyLeftEventArgs e) { K9DogTuning.Clear(); K9ScentTable.ClearHostTable(); _lastTuningSeen = string.Empty; _lastScentsSeen = string.Empty; Logger.Debug("K9 left the lobby; dog tuning and scent table reset to local values."); } private void OnJoinedAnyLobby() { try { _localPlayerCode = _client.LocalPlayerId64.ToString(K9Proto.Inv); } catch { } PublishModBeacon(); ReadHostKeys(); bool flag = SafeIsHost(); bool flag2 = K9Net.IsServer(); Logger.Msg($"K9 lobby ready. localPlayerCode={_localPlayerCode} steamSaysHost={flag} fishNetSaysServer={flag2}"); if (flag != flag2) { Logger.Warning($"K9: Steam and FishNet disagree about who is the host (steam={flag}, fishnet={flag2}). " + "FishNet's answer is the one being used. If K9 behaves oddly in this session, start here."); } } private void HandleMemberJoined(object s, MemberJoinedEventArgs e) { if (K9Net.IsServer()) { Logger.Debug("K9: member joined (" + SafeName(e) + "); host will re-publish keys and snapshot."); K9NetSync.OnMemberJoinedServerSide(); } } private void HandleLobbyDataChanged(object s, LobbyDataChangedEventArgs e) { try { if (e != null) { if (e.Key == "k9p.cfg") { ApplyTuningIfNew(e.NewValue); } else if (e.Key == "k9p.scents") { ApplyScentsIfNew(e.NewValue); } } } catch (Exception ex) { Logger.Debug("K9 lobby data change: " + ex.Message); } } private void ReadHostKeys() { try { string lobbyData = _client.GetLobbyData("k9p.proto"); if (!string.IsNullOrEmpty(lobbyData) && K9Proto.TryVersion(lobbyData, out var v) && v != 2) { Logger.Warning($"K9 PROTOCOL MISMATCH: this machine speaks {2}, the host speaks {v}. " + "K9 multiplayer will not work in this lobby until both machines run the same mod version."); } ApplyTuningIfNew(_client.GetLobbyData("k9p.cfg")); ApplyScentsIfNew(_client.GetLobbyData("k9p.scents")); } catch (Exception ex) { Logger.Debug("K9 could not read host keys: " + ex.Message); } } private void ApplyTuningIfNew(string payload) { if (!string.IsNullOrEmpty(payload) && !(payload == _lastTuningSeen)) { _lastTuningSeen = payload; if (!K9Net.IsServer()) { K9DogTuning.ApplyHostPayload(payload); } } } private void ApplyScentsIfNew(string payload) { if (!string.IsNullOrEmpty(payload) && !(payload == _lastScentsSeen)) { _lastScentsSeen = payload; if (!K9Net.IsServer()) { K9ScentTable.ApplyHostPayload(payload); } } } public void PublishHostKeys(string tuningPayload, string scentsPayload) { if (_client != null && _client.IsInLobby && K9Net.IsServer()) { _client.SetLobbyData("k9p.host", _localPlayerCode); _client.SetLobbyData("k9p.proto", 2.ToString(K9Proto.Inv)); _client.SetLobbyData("k9p.cfg", tuningPayload ?? string.Empty); _lastTuningSeen = tuningPayload ?? string.Empty; string text = scentsPayload ?? string.Empty; _client.SetLobbyData("k9p.scents", text); _lastScentsSeen = text; Logger.Msg($"K9 host keys published. host={_localPlayerCode} proto={2} " + "cfg=" + tuningPayload + " scents=" + DescribeScents(text)); } } private static string DescribeScents(string payload) { if (string.IsNullOrEmpty(payload)) { return "(none - clients stay on their own files)"; } try { string[] array = payload.Split('|'); if (array.Length != 7) { return $"(unrecognised, {payload.Length} chars)"; } return $"{Rows(array[4])}/{Rows(array[5])}/{Rows(array[6])} " + $"({Encoding.UTF8.GetByteCount(payload)} bytes)"; } catch { return "(undescribable)"; } static int Rows(string field) { int num = 0; for (int i = 0; i < field.Length; i++) { if (field[i] == '~') { num++; } } return num; } } public void PublishModBeacon() { if (_client != null && _client.IsInLobby) { _client.SetMyData("k9p.mod", string.Format("{0}/{1}", "1.1.0", 2)); Logger.Debug(string.Format("K9 mod beacon published: {0}/{1}", "1.1.0", 2)); } } public List GetMembersWithoutMod() { //IL_0087: Unknown result type (might be due to invalid IL or missing references) List list = new List(); if (_client == null || !_client.IsInLobby) { return list; } Dictionary dataForAllPlayers = _client.GetDataForAllPlayers("k9p.mod"); foreach (MemberInfo lobbyMember in _client.GetLobbyMembers()) { if (lobbyMember != null && lobbyMember.SteamId64 != _client.LocalPlayerId64 && (!dataForAllPlayers.TryGetValue(lobbyMember.SteamId, out var value) || string.IsNullOrEmpty(value))) { list.Add($"{lobbyMember.DisplayName} ({lobbyMember.SteamId64})"); } } return list; } public void SendContrabandToHost(uint seq, byte flags, byte minStealth, float scent) { //IL_0061: Unknown result type (might be due to invalid IL or missing references) if (_client == null || !_client.IsInLobby) { return; } MemberInfo hostMember = _client.GetHostMember(); if (hostMember == null) { Logger.Debug("K9 contraband: no host member yet."); return; } K9ContrabandMessage k9ContrabandMessage = new K9ContrabandMessage(seq, flags, minStealth, scent); _client.P2PManager.SendMessageAsync(hostMember.SteamId, (P2PMessage)(object)k9ContrabandMessage, 77, (EP2PSend)2)?.ContinueWith(delegate(Task x) { if (x.IsFaulted) { Logger.Debug("K9 contraband send faulted: " + x.Exception?.GetBaseException().Message); } }, TaskContinuationOptions.OnlyOnFaulted); Logger.Debug($"K9 contraband published seq={seq} flags={flags} minStealth={minStealth} scent={scent:0.###} -> host {hostMember.SteamId64}"); } public void BroadcastEvent(byte kind, int officerId, string officerGuid) { if (_client != null && _client.IsInLobby) { K9EventMessage k9EventMessage = new K9EventMessage((K9EventMessage.Kind)kind, officerId, officerGuid); _client.P2PManager.BroadcastMessage((P2PMessage)(object)k9EventMessage, 77, (EP2PSend)2); Logger.Debug($"K9 event broadcast kind={kind} officer={officerId}"); } } public void BroadcastUnit(byte op, int officerId, string officerGuid, byte state, string targetPlayerCode) { if (_client != null && _client.IsInLobby) { K9UnitMessage k9UnitMessage = new K9UnitMessage((K9UnitMessage.Op)op, officerId, officerGuid, state, targetPlayerCode); _client.P2PManager.BroadcastMessage((P2PMessage)(object)k9UnitMessage, 77, (EP2PSend)2); Logger.Debug($"K9 unit broadcast op={op} officer={officerId} state={state} target='{targetPlayerCode}'"); } } public void BroadcastSnapshot(List entries) { if (_client == null || !_client.IsInLobby) { return; } List list = new List(entries?.Count ?? 0); if (entries != null) { foreach (K9SnapshotEntry entry in entries) { list.Add(new K9SnapshotMessage.Entry { OfficerId = entry.OfficerId, OfficerGuid = entry.OfficerGuid, State = entry.State, TargetPlayerCode = entry.TargetPlayerCode }); } } K9SnapshotMessage k9SnapshotMessage = new K9SnapshotMessage(list); try { int maxPacketSize = _client.P2PManager.GetMaxPacketSize((EP2PSend)0); byte[] array = ((P2PMessage)k9SnapshotMessage).Serialize(); int num = ((array != null) ? array.Length : 0); if (num > maxPacketSize) { Logger.Warning($"K9 snapshot is {num} bytes against a {maxPacketSize}-byte limit for {list.Count} units - " + "sending reliable instead. If this appears, the snapshot needs splitting."); _client.P2PManager.BroadcastMessage((P2PMessage)(object)k9SnapshotMessage, 77, (EP2PSend)2); return; } } catch { } _client.P2PManager.BroadcastMessage((P2PMessage)(object)k9SnapshotMessage, 77, (EP2PSend)0); } private void HandleContraband(K9ContrabandMessage msg, CSteamID sender) { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Unknown result type (might be due to invalid IL or missing references) try { if (msg == null || !msg.Accepted) { WarnProtoOnce(sender, "k9p.contraband"); } else if (K9Net.IsServer()) { string arg = K9SteamLink.SteamIdToPlayerCode(sender.m_SteamID); K9SteamLink.OnContraband?.Invoke(arg, msg.Seq, msg.Flags, msg.MinStealth, msg.Scent); } } catch (Exception ex) { Logger.Debug("K9 contraband handler: " + ex.Message); } } private bool IsFromHost(CSteamID sender) { //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Unknown result type (might be due to invalid IL or missing references) try { SteamNetworkClient client = _client; MemberInfo val = ((client != null) ? client.GetHostMember() : null); if (val == null) { return false; } return val.SteamId == sender; } catch { return false; } } private void WarnImpostorOnce(CSteamID sender, string type) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Unknown result type (might be due to invalid IL or missing references) if (_impostorWarned.Add(sender.m_SteamID)) { Logger.Warning($"K9 ignored a '{type}' message from {sender.m_SteamID}, which is not the host of this lobby. " + "Only the host may tell this machine what K9 units exist. Further messages from this peer will be dropped silently."); } } private void HandleUnit(K9UnitMessage msg, CSteamID sender) { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Unknown result type (might be due to invalid IL or missing references) try { if (msg == null || !msg.Accepted) { WarnProtoOnce(sender, "k9p.unit"); } else if (!K9Net.IsServer()) { if (!IsFromHost(sender)) { WarnImpostorOnce(sender, "k9p.unit"); } else { K9SteamLink.OnUnit?.Invoke(msg.OpCode, msg.OfficerId, msg.OfficerGuid, msg.State, msg.TargetPlayerCode); } } } catch (Exception ex) { Logger.Debug("K9 unit handler: " + ex.Message); } } private void HandleEvent(K9EventMessage msg, CSteamID sender) { //IL_0017: Unknown result type (might be due to invalid IL or missing references) try { if (msg == null || !msg.Accepted) { WarnProtoOnce(sender, "k9p.event"); } else { K9SteamLink.OnEvent?.Invoke(msg.KindCode, msg.OfficerId, msg.OfficerGuid); } } catch (Exception ex) { Logger.Debug("K9 event handler: " + ex.Message); } } private void HandleSnapshot(K9SnapshotMessage msg, CSteamID sender) { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_0046: Unknown result type (might be due to invalid IL or missing references) try { if (msg == null || !msg.Accepted) { WarnProtoOnce(sender, "k9p.snapshot"); } else { if (K9Net.IsServer()) { return; } if (!IsFromHost(sender)) { WarnImpostorOnce(sender, "k9p.snapshot"); return; } List list = new List(msg.Entries.Count); foreach (K9SnapshotMessage.Entry entry in msg.Entries) { list.Add(new K9SnapshotEntry { OfficerId = entry.OfficerId, OfficerGuid = entry.OfficerGuid, State = entry.State, TargetPlayerCode = entry.TargetPlayerCode }); } K9SteamLink.OnSnapshot?.Invoke(list); } } catch (Exception ex) { Logger.Debug("K9 snapshot handler: " + ex.Message); } } private void WarnProtoOnce(CSteamID sender, string type) { //IL_0034: Unknown result type (might be due to invalid IL or missing references) string item = sender.m_SteamID + ":" + type; if (_warned.Add(item)) { Logger.Warning($"K9 rejected a '{type}' message from {sender.m_SteamID} - wrong protocol version or malformed payload. " + $"This machine speaks protocol {2}. Further rejections from this peer will be silent."); } } private bool SafeIsHost() { try { return _client != null && _client.IsHost; } catch { return false; } } private static string SafeName(MemberJoinedEventArgs e) { try { object obj; if (e == null) { obj = null; } else { MemberInfo member = e.Member; obj = ((member != null) ? member.DisplayName : null); } if (obj == null) { obj = "?"; } return (string)obj; } catch { return "?"; } } } internal static class K9Proto { internal const int Version = 2; internal static readonly CultureInfo Inv = CultureInfo.InvariantCulture; internal static bool TryVersion(string s, out int v) { return int.TryParse(s, NumberStyles.Integer, Inv, out v); } } internal sealed class K9ContrabandMessage : P2PMessage { internal const string TypeName = "k9p.contraband"; public uint Seq; public byte Flags; public byte MinStealth; public float Scent; public override string MessageType => "k9p.contraband"; public bool Accepted { get; private set; } internal bool Carrying => (Flags & 1) != 0; public K9ContrabandMessage() { } internal K9ContrabandMessage(uint seq, byte flags, byte minStealth, float scent) { Seq = seq; Flags = flags; MinStealth = minStealth; Scent = scent; Accepted = true; } public override byte[] Serialize() { return Encoding.UTF8.GetBytes(string.Join("|", 2.ToString(K9Proto.Inv), Seq.ToString(K9Proto.Inv), Flags.ToString(K9Proto.Inv), MinStealth.ToString(K9Proto.Inv), Scent.ToString("0.###", K9Proto.Inv))); } public override void Deserialize(byte[] data) { Accepted = false; try { string[] array = Encoding.UTF8.GetString(data).Split('|'); if (array.Length == 5 && K9Proto.TryVersion(array[0], out var v) && v == 2 && uint.TryParse(array[1], NumberStyles.Integer, K9Proto.Inv, out var result) && byte.TryParse(array[2], NumberStyles.Integer, K9Proto.Inv, out var result2) && byte.TryParse(array[3], NumberStyles.Integer, K9Proto.Inv, out var result3) && float.TryParse(array[4], NumberStyles.Float, K9Proto.Inv, out var result4)) { Seq = result; Flags = result2; MinStealth = result3; Scent = result4; Accepted = true; } } catch { } } } internal sealed class K9UnitMessage : P2PMessage { internal enum Op : byte { Spawn = 1, State, Despawn } internal const string TypeName = "k9p.unit"; public byte OpCode; public int OfficerId; public string OfficerGuid = string.Empty; public byte State; public string TargetPlayerCode = string.Empty; public override string MessageType => "k9p.unit"; public bool Accepted { get; private set; } public K9UnitMessage() { } internal K9UnitMessage(Op op, int officerId, string officerGuid, byte state, string targetPlayerCode) { OpCode = (byte)op; OfficerId = officerId; OfficerGuid = officerGuid ?? string.Empty; State = state; TargetPlayerCode = targetPlayerCode ?? string.Empty; Accepted = true; } public override byte[] Serialize() { return Encoding.UTF8.GetBytes(string.Join("|", 2.ToString(K9Proto.Inv), OpCode.ToString(K9Proto.Inv), OfficerId.ToString(K9Proto.Inv), OfficerGuid, State.ToString(K9Proto.Inv), TargetPlayerCode)); } public override void Deserialize(byte[] data) { Accepted = false; try { string[] array = Encoding.UTF8.GetString(data).Split('|'); if (array.Length == 6 && K9Proto.TryVersion(array[0], out var v) && v == 2 && byte.TryParse(array[1], NumberStyles.Integer, K9Proto.Inv, out var result) && int.TryParse(array[2], NumberStyles.Integer, K9Proto.Inv, out var result2) && byte.TryParse(array[4], NumberStyles.Integer, K9Proto.Inv, out var result3)) { OpCode = result; OfficerId = result2; OfficerGuid = array[3]; State = result3; TargetPlayerCode = array[5]; Accepted = true; } } catch { } } } internal sealed class K9EventMessage : P2PMessage { internal enum Kind : byte { Yelp = 1 } internal const string TypeName = "k9p.event"; public byte KindCode; public int OfficerId; public string OfficerGuid = string.Empty; public override string MessageType => "k9p.event"; public bool Accepted { get; private set; } public K9EventMessage() { } internal K9EventMessage(Kind kind, int officerId, string officerGuid) { KindCode = (byte)kind; OfficerId = officerId; OfficerGuid = officerGuid ?? string.Empty; Accepted = true; } public override byte[] Serialize() { return Encoding.UTF8.GetBytes(string.Join("|", 2.ToString(K9Proto.Inv), KindCode.ToString(K9Proto.Inv), OfficerId.ToString(K9Proto.Inv), OfficerGuid)); } public override void Deserialize(byte[] data) { Accepted = false; try { string[] array = Encoding.UTF8.GetString(data).Split('|'); if (array.Length == 4 && K9Proto.TryVersion(array[0], out var v) && v == 2 && byte.TryParse(array[1], NumberStyles.Integer, K9Proto.Inv, out var result) && int.TryParse(array[2], NumberStyles.Integer, K9Proto.Inv, out var result2)) { KindCode = result; OfficerId = result2; OfficerGuid = array[3]; Accepted = true; } } catch { } } } internal sealed class K9SnapshotMessage : P2PMessage { internal struct Entry { internal int OfficerId; internal string OfficerGuid; internal byte State; internal string TargetPlayerCode; } internal const string TypeName = "k9p.snapshot"; public List Entries = new List(); public override string MessageType => "k9p.snapshot"; public bool Accepted { get; private set; } public K9SnapshotMessage() { } internal K9SnapshotMessage(List entries) { Entries = entries ?? new List(); Accepted = true; } public override byte[] Serialize() { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append(2.ToString(K9Proto.Inv)); foreach (Entry entry in Entries) { stringBuilder.Append('|').Append(entry.OfficerId.ToString(K9Proto.Inv)).Append('~') .Append(entry.OfficerGuid ?? string.Empty) .Append('~') .Append(entry.State.ToString(K9Proto.Inv)) .Append('~') .Append(entry.TargetPlayerCode ?? string.Empty); } return Encoding.UTF8.GetBytes(stringBuilder.ToString()); } public override void Deserialize(byte[] data) { Accepted = false; try { string[] array = Encoding.UTF8.GetString(data).Split('|'); if (array.Length < 1 || !K9Proto.TryVersion(array[0], out var v) || v != 2) { return; } List list = new List(array.Length - 1); for (int i = 1; i < array.Length; i++) { string[] array2 = array[i].Split('~'); if (array2.Length != 4 || !int.TryParse(array2[0], NumberStyles.Integer, K9Proto.Inv, out var result) || !byte.TryParse(array2[2], NumberStyles.Integer, K9Proto.Inv, out var result2)) { return; } list.Add(new Entry { OfficerId = result, OfficerGuid = array2[1], State = result2, TargetPlayerCode = array2[3] }); } Entries = list; Accepted = true; } catch { } } } internal static class K9ContrabandRegistry { internal struct Report { internal bool Carrying; internal byte MinStealth; internal float Scent; internal uint Seq; internal float ReceivedAt; } internal const byte NO_STEALTH = byte.MaxValue; private const float STALE_AFTER = 15f; private static readonly Dictionary _byPlayerCode = new Dictionary(); private static readonly HashSet _staleWarned = new HashSet(); internal const string LOCAL_SENTINEL = "@local"; internal static void Submit(string playerCode, uint seq, bool carrying, byte minStealth, float scent) { if (string.IsNullOrEmpty(playerCode) || playerCode == "0") { return; } if (_byPlayerCode.TryGetValue(playerCode, out var value) && seq != 0 && value.Seq != 0 && seq <= value.Seq) { if (!(Time.unscaledTime - value.ReceivedAt > 15f)) { return; } Logger.Debug($"K9 contraband {playerCode} restarted its sequence ({value.Seq} -> {seq}) after " + $"{Time.unscaledTime - value.ReceivedAt:0}s of silence; accepting it as a new sender."); } _byPlayerCode[playerCode] = new Report { Carrying = carrying, MinStealth = minStealth, Scent = scent, Seq = seq, ReceivedAt = Time.unscaledTime }; _staleWarned.Remove(playerCode); if (value.Carrying != carrying) { Logger.Debug($"K9 contraband {playerCode} -> {carrying} (seq={seq}, minStealth={minStealth}, scent={scent:0.00})"); } } internal static void Forget(string playerCode) { if (!string.IsNullOrEmpty(playerCode)) { _byPlayerCode.Remove(playerCode); _staleWarned.Remove(playerCode); } } internal static void Clear() { _byPlayerCode.Clear(); _staleWarned.Clear(); } internal static bool HasContraband(Player player, PoliceOfficer officer) { if ((Object)(object)player == (Object)null) { return false; } string text = PlayerCodeOf(player); if (string.IsNullOrEmpty(text)) { return false; } if (!_byPlayerCode.TryGetValue(text, out var value)) { return false; } if (Time.unscaledTime - value.ReceivedAt > 15f) { if (_staleWarned.Add(text)) { Logger.Warning($"K9 has heard nothing from player {text} for over {15f:0}s. " + "They are being treated as carrying nothing. If they are in the lobby and playing, either their K9 Patrol is not running or its packets are not arriving."); } return false; } if (!value.Carrying) { return false; } return true; } internal static float SniffRadiusFactor(Player player) { if ((Object)(object)player == (Object)null) { return 0f; } string text = PlayerCodeOf(player); if (string.IsNullOrEmpty(text)) { return 0f; } if (!_byPlayerCode.TryGetValue(text, out var value)) { return 0f; } if (Time.unscaledTime - value.ReceivedAt > 15f) { return 0f; } return K9ScentTable.RadiusMultiplier(value.Scent); } internal static float ScentOf(Player player) { if ((Object)(object)player == (Object)null) { return 0f; } string text = PlayerCodeOf(player); if (!string.IsNullOrEmpty(text) && _byPlayerCode.TryGetValue(text, out var value)) { return value.Scent; } return 0f; } internal static string PlayerCodeOf(Player p) { if ((Object)(object)p == (Object)null) { return string.Empty; } try { string playerCode = p.PlayerCode; if (!string.IsNullOrEmpty(playerCode) && playerCode != "0") { return playerCode; } if (IsLocal(p)) { return "@local"; } return string.Empty; } catch { return string.Empty; } } private static bool IsLocal(Player p) { try { return (Object)(object)p != (Object)null && (Object)(object)p == (Object)(object)Player.Local; } catch { return false; } } } internal static class K9ContrabandPublisher { private struct ScanTotals { internal bool Carrying; internal byte MinStealth; internal float Scent; } private const float EVAL_INTERVAL = 0.5f; private const float HEARTBEAT_INTERVAL = 5f; private static float _nextEval; private static float _nextHeartbeat; private static uint _seq; private static bool _lastCarrying; private static byte _lastMinStealth = byte.MaxValue; private static float _lastScent; private static bool _everPublished; private static readonly string SlotLinePrefix = Environment.NewLine + " "; private const float CONTAINER_RESCAN_INTERVAL = 5f; private static StorageEntity[] _containers; private static Player _containersFor; private static float _containersFreshUntil; internal static void Reset() { _nextEval = 0f; _nextHeartbeat = 0f; _seq = 0u; _lastCarrying = false; _lastMinStealth = byte.MaxValue; _lastScent = 0f; _everPublished = false; } internal static void Tick() { float unscaledTime = Time.unscaledTime; if (unscaledTime < _nextEval) { return; } _nextEval = unscaledTime + 0.5f; Player val = SafeLocalPlayer(); if ((Object)(object)val == (Object)null) { return; } string text = K9ContrabandRegistry.PlayerCodeOf(val); if (string.IsNullOrEmpty(text)) { return; } Scan(out var carrying, out var minStealth, out var scent); bool flag = !_everPublished || carrying != _lastCarrying || minStealth != _lastMinStealth || Mathf.Abs(scent - _lastScent) > 0.05f; bool flag2 = unscaledTime >= _nextHeartbeat; if (flag || flag2) { _lastCarrying = carrying; _lastMinStealth = minStealth; _lastScent = scent; _everPublished = true; _nextHeartbeat = unscaledTime + 5f; _seq++; K9ContrabandRegistry.Submit(text, _seq, carrying, minStealth, scent); if (!K9Net.IsServer() && K9SteamLink.Available && K9SteamLink.IsInLobby) { byte flags = (byte)(carrying ? 1u : 0u); K9SteamLink.SendContrabandToHost(_seq, flags, minStealth, scent); } if (flag) { Logger.Debug(string.Format("K9 local contraband {0} minStealth={1} scent={2:0.###} seq={3}", carrying ? "YES" : "no", minStealth, scent, _seq)); } } } private static void ScoreSlot(ItemSlot slot, ref ScanTotals totals, float weight, string where, int index, StringBuilder log) { //IL_01c1: Unknown result type (might be due to invalid IL or missing references) //IL_01c7: Invalid comparison between Unknown and I4 //IL_007a: Unknown result type (might be due to invalid IL or missing references) ItemInstance val = ((slot != null) ? slot.ItemInstance : null); if (val == null) { return; } int num = Mathf.Max(1, ((BaseItemInstance)val).Quantity); string text = ((weight < 1f) ? $" x {weight:0.##} contained" : ""); ProductItemInstance val2 = K9Cast.AsProduct(val); if (val2 != null) { totals.Carrying = true; PackagingDefinition appliedPackaging = val2.AppliedPackaging; byte b = (byte)((!((Object)(object)appliedPackaging == (Object)null)) ? ((byte)appliedPackaging.StealthLevel) : 0); if (b < totals.MinStealth) { totals.MinStealth = b; } string text2 = (((Object)(object)appliedPackaging == (Object)null) ? "(none)" : ((BaseItemDefinition)appliedPackaging).ID); int num2 = (((Object)(object)appliedPackaging == (Object)null) ? 1 : Mathf.Max(1, appliedPackaging.Quantity)); int num3 = num * num2; float num4 = K9ScentTable.SmellOf(((BaseItemInstance)val2).ID, isProduct: true); float num5 = K9ScentTable.PackagingMultiplier(text2); float num6 = num4 * (float)num3 * num5 * weight; totals.Scent += num6; log?.Append(SlotLinePrefix).Append($"{where} {index}: {((BaseItemInstance)val2).ID} x{num} in '{text2}' " + $"({num2}/pack = {num3} units) smell {num4:0.###} x {num5:0.##} hidden" + $"{text} -> {num6:0.###}"); } else { ItemDefinition definition = val.Definition; if ((Object)(object)definition != (Object)null && (int)((BaseItemDefinition)definition).legalStatus > 0) { totals.Carrying = true; totals.MinStealth = 0; float num7 = K9ScentTable.SmellOf(((BaseItemDefinition)definition).ID, isProduct: false); float num8 = num7 * (float)num * weight; totals.Scent += num8; log?.Append(SlotLinePrefix).Append($"{where} {index}: {((BaseItemDefinition)definition).ID} x{num} (illegal, unpackaged) " + $"smell {num7:0.###}{text} -> {num8:0.###}"); } } } private static void Scan(out bool carrying, out byte minStealth, out float scent) { ScanTotals totals = new ScanTotals { Carrying = false, MinStealth = byte.MaxValue, Scent = 0f }; K9ScentTable.DiscoverOnce(); bool value = K9Config.DebugLogging.Value; StringBuilder stringBuilder = (value ? new StringBuilder() : null); try { PlayerInventory instance = PlayerSingleton.Instance; if ((Object)(object)instance != (Object)null && instance.hotbarSlots != null) { int num = -1; foreach (HotbarSlot hotbarSlot in instance.hotbarSlots) { num++; ScoreSlot((ItemSlot)(object)hotbarSlot, ref totals, 1f, "slot", num, stringBuilder); } } ScanContainers(ref totals, stringBuilder); } catch (Exception ex) { Logger.Debug("K9 contraband scan threw (" + ex.Message + "); reporting what it had scored before that point."); } carrying = totals.Carrying; minStealth = totals.MinStealth; scent = totals.Scent; if (value && stringBuilder != null && stringBuilder.Length > 0) { Logger.Debug($"K9 scent scan: total {scent:0.###} " + $"(full radius at {K9Config.ScentFullRadiusScore.Value:0.##}, " + $"so x{K9ScentTable.RadiusMultiplier(scent):0.##} of SniffRadius){stringBuilder}"); } } private static StorageEntity[] Containers(Player local) { bool flag = _containers == null || (Object)(object)_containersFor != (Object)(object)local || Time.time >= _containersFreshUntil; if (!flag) { StorageEntity[] containers = _containers; foreach (StorageEntity val in containers) { if ((Object)(object)val == (Object)null) { flag = true; break; } } } if (flag) { try { _containers = (((Object)(object)local == (Object)null) ? Array.Empty() : ((Component)local).GetComponentsInChildren(true)); } catch { _containers = Array.Empty(); } if (_containers == null) { _containers = Array.Empty(); } _containersFor = local; _containersFreshUntil = Time.time + 5f; } return _containers; } private static void ScanContainers(ref ScanTotals totals, StringBuilder log) { float backpackWeight = K9ScentTable.BackpackWeight; if (backpackWeight <= 0f) { return; } Player val = SafeLocalPlayer(); if ((Object)(object)val == (Object)null) { return; } StorageEntity[] array = Containers(val); foreach (StorageEntity val2 in array) { if ((Object)(object)val2 == (Object)null || val2.ItemSlots == null) { continue; } string text = (string.IsNullOrEmpty(val2.StorageEntityName) ? "container" : val2.StorageEntityName); int num = -1; foreach (ItemSlot itemSlot in val2.ItemSlots) { num++; ScoreSlot(itemSlot, ref totals, backpackWeight, text, num, log); } } } private static Player SafeLocalPlayer() { try { return Player.Local; } catch { return null; } } } } namespace K9_Patrol.Source.Controllers { public sealed class K9UnitController : MonoBehaviour { private enum SearchOutcome { Unknown, Clear, ItemsFound } private static readonly Dictionary _lastSearchAt = new Dictionary(); private bool _ready; private float _setupTimer = 0f; internal Player _pursuitTarget; private Player _currentSearchTarget; private float _detectionTick; private float _stationCheckTick; private const float STATION_CHECK_INTERVAL = 0.5f; private bool _officerHasLeftStation; private float _outsideSince = -1f; private float _spawnedAt; private const float STATION_EXIT_GRACE = 45f; private const float MIN_PATROL_ARM = 10f; private bool _isSearchListenerActive; private SearchOutcome _searchOutcome = SearchOutcome.Unknown; private bool _searchObservedActive; private float _searchBeganAt; private const float SEARCH_ACTIVATION_TIMEOUT = 20f; private UnityAction _onItemsFound; private UnityAction _onClear; private bool _alertAnnounced; private bool _speedBoostApplied; private float _origMoveSpeedMultiplier = 1f; private K9NetSync.K9NetState _lastNetState = K9NetSync.K9NetState.Following; private float _lastNetStateTime; private float _lastBeginSearchTime; private const float MIN_SEARCH_BEGIN_INTERVAL = 0.5f; private float _pursuitAcquireRadius = 10f; private const float PURSUIT_RELEASE_HYSTERESIS = 1.35f; private const float INVESTIGATE_BROADCAST_DISTANCE = 6f; private const string HarnessObjectName = "K9Harness"; private const float OCCLUDED_SNIFF_FACTOR = 0.35f; private static int _visionBlockingMask = -1; private static bool _visionMaskWarned; private const float DETECTION_LOG_INTERVAL = 5f; private static readonly Dictionary _lastDetectionLog = new Dictionary(); public PoliceOfficer Officer { get; private set; } public GameObject Dog { get; private set; } public K9NPC K9DogNPC { get; private set; } private float BodySearchStartDistance => K9Config.BodySearchStartDistance.Value; private bool _currentSearchTargetIsActive => (Object)(object)Officer != (Object)null && (Object)(object)Officer.BodySearchBehaviour != (Object)null && ((Behaviour)Officer.BodySearchBehaviour).Active; private static bool IsServer() { return K9Net.IsServer(); } private void NetState(K9NetSync.K9NetState s, Player target = null, bool force = false) { if (IsServer() && (force || s != _lastNetState || !(Time.time - _lastNetStateTime < 0.25f))) { K9NetSync.ServerState(Officer, s, target); _lastNetState = s; _lastNetStateTime = Time.time; } } public void Initialize(PoliceOfficer officer) { Officer = officer; ((Object)this).name = $"K9_ofc_{((Object)officer).GetInstanceID()}"; _spawnedAt = Time.time; CreateDogFromPrefab(); _setupTimer = 0.25f; } private void Update() { if (!_ready && _setupTimer > 0f) { _setupTimer -= Time.deltaTime; if (_setupTimer <= 0f) { FinalizeSetup(); } } if (!_ready || (Object)(object)Officer == (Object)null) { return; } _stationCheckTick += Time.deltaTime; if (_stationCheckTick >= 0.5f) { _stationCheckTick = 0f; if (!K9Manager.IsOfficerInStation(Officer)) { if (_outsideSince < 0f) { _outsideSince = Time.time; } if (!_officerHasLeftStation && Time.time - _outsideSince >= 10f) { _officerHasLeftStation = true; Logger.Debug($"K9 unit for officer {((Object)Officer).GetInstanceID()} is on patrol " + $"(officer has been out of a station for {10f:0}s)."); } } else { if (_officerHasLeftStation) { _outsideSince = -1f; float num = Time.time - _spawnedAt; if (num < 15f) { Logger.Warning($"K9 unit for officer {((Object)Officer).GetInstanceID()} ended after only {num:0.0}s, " + $"despite the {10f:0}s patrol arm. Something other than a normal " + "patrol is moving this officer in and out of a station."); } else { Logger.Debug($"K9 unit for officer {((Object)Officer).GetInstanceID()} ended: officer returned to a station after {num:0.0}s."); } if (IsServer()) { K9NetSync.ServerDespawn(Officer); } Object.Destroy((Object)(object)((Component)this).gameObject); return; } if (Time.time - _spawnedAt > 45f) { _outsideSince = -1f; Logger.Debug($"Officer {((Object)Officer).GetInstanceID()} never left a station in {45f}s; releasing the K9 unit."); if (IsServer()) { K9NetSync.ServerDespawn(Officer); } Object.Destroy((Object)(object)((Component)this).gameObject); return; } } } if (IsServer()) { _detectionTick += Time.deltaTime; if (_detectionTick >= K9Config.RecheckInterval.Value) { _detectionTick = 0f; TryDetectAndAct(); } if (_isSearchListenerActive && (Object)(object)_currentSearchTarget == (Object)null) { Logger.Debug("The player being searched no longer exists (disconnected or despawned); concluding the search so the unit is not held on a target that cannot answer."); HandleSearchComplete(); } if (!((Object)(object)_pursuitTarget != (Object)null)) { return; } MaintainPursuit(); if ((Object)(object)Officer != (Object)null && (Object)(object)Officer.BodySearchBehaviour != (Object)null && ((Behaviour)Officer.BodySearchBehaviour).Active && (Object)(object)_currentSearchTarget != (Object)null) { FaceOfficerTowardTarget(_currentSearchTarget); FaceDogTowardTarget(_currentSearchTarget); } if (_isSearchListenerActive && (Object)(object)_currentSearchTarget != (Object)null) { if (_currentSearchTargetIsActive) { _searchObservedActive = true; } else if (_searchObservedActive) { HandleSearchComplete(); } else if (Time.time - _searchBeganAt > 20f) { Logger.Debug("Body search never became active; releasing target."); HandleSearchComplete(); } } } else if ((Object)(object)Officer != (Object)null && (Object)(object)Officer.BodySearchBehaviour != (Object)null && ((Behaviour)Officer.BodySearchBehaviour).Active && (Object)(object)_currentSearchTarget != (Object)null) { FaceDogTowardTarget(_currentSearchTarget); } } internal void FinalizeSetup() { if ((Object)(object)K9DogNPC != (Object)null) { K9DogNPC._officer = Officer; } _ready = true; if (IsServer()) { NetState(K9NetSync.K9NetState.Following, null, force: true); } } private void CreateDogFromPrefab() { K9DogNPC = K9DogFactory.Build(Officer, $"K9_Dog_{((Object)Officer).GetInstanceID()}"); Dog = (((Object)(object)K9DogNPC != (Object)null) ? ((Component)K9DogNPC).gameObject : null); } internal static void ApplyDogMaterialAndVisualTweaks(GameObject dogRoot) { //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_00cb: Unknown result type (might be due to invalid IL or missing references) //IL_00ea: Unknown result type (might be due to invalid IL or missing references) try { if ((Object)(object)dogRoot == (Object)null || (Object)(object)Core.bundle == (Object)null) { return; } Texture2D albedo = K9Assets.LoadTexture("Assets/Bublisher/3D Stylized Animated Dogs Kit/Textures/3D Stylized Animated Dogs Kit - BaseColor_Custom.png"); Texture2D normal = K9Assets.LoadTexture("Assets/Bublisher/3D Stylized Animated Dogs Kit/Textures/3D Stylized Animated Dogs Kit - Normal.png"); Material val = MakeMaterial(albedo, normal); if ((Object)(object)val == (Object)null) { return; } dogRoot.transform.localScale = Vector3.one * 1.3f; Material val2 = null; Material val3 = null; Renderer[] componentsInChildren = dogRoot.GetComponentsInChildren(true); foreach (Renderer val4 in componentsInChildren) { if (((Object)((Component)val4).gameObject).name == "K9Harness") { if ((Object)(object)val2 == (Object)null) { val2 = MakeColourMaterial(new Color(0.1f, 0.1f, 0.12f), 0.35f); val3 = MakeColourMaterial(new Color(0.9f, 0.78f, 0.16f), 0.2f); } if (!((Object)(object)val2 == (Object)null)) { Material[] sharedMaterials = val4.sharedMaterials; for (int j = 0; j < sharedMaterials.Length; j++) { sharedMaterials[j] = ((j == 1) ? val3 : val2); } val4.sharedMaterials = sharedMaterials; } } else { val4.sharedMaterial = val; } } } catch { } } private static Material MakeColourMaterial(Color colour, float smoothness) { //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Expected O, but got Unknown //IL_0066: Unknown result type (might be due to invalid IL or missing references) //IL_0084: Unknown result type (might be due to invalid IL or missing references) Shader val = Shader.Find("Universal Render Pipeline/Lit") ?? Shader.Find("Standard") ?? Shader.Find("Unlit/Color") ?? Shader.Find("Diffuse"); if ((Object)(object)val == (Object)null) { return null; } Material val2 = new Material(val); if (val2.HasProperty("_BaseColor")) { val2.SetColor("_BaseColor", colour); } if (val2.HasProperty("_Color")) { val2.SetColor("_Color", colour); } if (val2.HasProperty("_Metallic")) { val2.SetFloat("_Metallic", 0f); } if (val2.HasProperty("_Smoothness")) { val2.SetFloat("_Smoothness", smoothness); } if (val2.HasProperty("_Glossiness")) { val2.SetFloat("_Glossiness", smoothness); } return val2; } private static Material MakeMaterial(Texture2D albedo, Texture2D normal) { //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Expected O, but got Unknown Shader val = Shader.Find("Universal Render Pipeline/Lit") ?? Shader.Find("Standard") ?? Shader.Find("Unlit/Texture") ?? Shader.Find("Diffuse"); if ((Object)(object)val == (Object)null || (Object)(object)albedo == (Object)null) { return null; } Material val2 = new Material(val); if (val2.HasProperty("_BaseMap")) { val2.SetTexture("_BaseMap", (Texture)(object)albedo); } if (val2.HasProperty("_MainTex")) { val2.SetTexture("_MainTex", (Texture)(object)albedo); } if (Object.op_Implicit((Object)(object)normal) && val2.HasProperty("_BumpMap")) { val2.SetTexture("_BumpMap", (Texture)(object)normal); val2.EnableKeyword("_NORMALMAP"); } if (val2.HasProperty("_Metallic")) { val2.SetFloat("_Metallic", 0f); } if (val2.HasProperty("_Smoothness")) { val2.SetFloat("_Smoothness", 0.1f); } return val2; } private static int VisionBlockingMask(PoliceOfficer officer) { if (_visionBlockingMask >= 0) { return _visionBlockingMask; } try { VisionCone val = (((Object)(object)officer != (Object)null && (Object)(object)((NPC)officer).Awareness != (Object)null) ? ((NPC)officer).Awareness.VisionCone : null); if ((Object)(object)val != (Object)null) { _visionBlockingMask = ((LayerMask)(ref val.VisibilityBlockingLayers)).value; Logger.Debug($"K9 occlusion will cast against the game's own VisibilityBlockingLayers ({_visionBlockingMask})."); return _visionBlockingMask; } } catch { } if (!_visionMaskWarned) { _visionMaskWarned = true; Logger.Debug("K9 could not read VisionCone.VisibilityBlockingLayers from an officer yet; occlusion is not reducing the sniff radius until it can."); } return 0; } private bool IsOccludedFrom(Player target) { //IL_00b2: Unknown result type (might be due to invalid IL or missing references) //IL_00b7: 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_00c6: Unknown result type (might be due to invalid IL or missing references) //IL_00cb: Unknown result type (might be due to invalid IL or missing references) //IL_009f: Unknown result type (might be due to invalid IL or missing references) //IL_00a4: Unknown result type (might be due to invalid IL or missing references) //IL_00d2: Unknown result type (might be due to invalid IL or missing references) //IL_00d7: Unknown result type (might be due to invalid IL or missing references) //IL_00e1: Unknown result type (might be due to invalid IL or missing references) //IL_00e6: Unknown result type (might be due to invalid IL or missing references) //IL_00eb: Unknown result type (might be due to invalid IL or missing references) //IL_00ec: Unknown result type (might be due to invalid IL or missing references) //IL_00ed: Unknown result type (might be due to invalid IL or missing references) try { if ((Object)(object)target == (Object)null || (Object)(object)((Component)target).transform == (Object)null || (Object)(object)Officer == (Object)null) { return false; } int num = VisionBlockingMask(Officer); if (num == 0) { return false; } VisionCone val = (((Object)(object)((NPC)Officer).Awareness != (Object)null) ? ((NPC)Officer).Awareness.VisionCone : null); Vector3 val2 = ((!((Object)(object)val != (Object)null) || !((Object)(object)val.VisionOrigin != (Object)null)) ? (((Component)Officer).transform.position + Vector3.up * 1.6f) : val.VisionOrigin.position); Vector3 val3 = ((Component)target).transform.position + Vector3.up * 1f; return Physics.Linecast(val2, val3, num, (QueryTriggerInteraction)1); } catch { return false; } } private void LogDetection(Player p, float distance, float factor, float effective, bool occluded, bool inRange) { if (!K9Config.DebugLogging.Value) { return; } try { int instanceID = ((Object)p).GetInstanceID(); float time = Time.time; bool flag = true; if (_lastDetectionLog.TryGetValue(instanceID, out (bool, float) value)) { flag = value.Item1 != inRange || time - value.Item2 >= 5f; } if (flag) { _lastDetectionLog[instanceID] = (inRange, time); float num = K9ContrabandRegistry.ScentOf(p); Logger.Debug("K9 detect [officer " + ((Object)Officer).name + "] player=" + K9ContrabandRegistry.PlayerCodeOf(p) + " " + $"dist={distance:0.0}m scent={num:0.###} -> x{factor:0.00} of SniffRadius " + $"({K9Config.SniffRadius.Value:0.#}m) = {effective / (occluded ? 0.35f : 1f):0.0}m" + (occluded ? $", OCCLUDED so x{0.35f:0.00} = {effective:0.0}m" : ", clear line") + " => " + (inRange ? "DETECTED" : "not detected")); } } catch { } } private void TryDetectAndAct() { //IL_00d6: Unknown result type (might be due to invalid IL or missing references) //IL_00e2: Unknown result type (might be due to invalid IL or missing references) //IL_00e7: Unknown result type (might be due to invalid IL or missing references) //IL_00ec: Unknown result type (might be due to invalid IL or missing references) //IL_0295: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)Officer == (Object)null || (Object)(object)((NPC)Officer).Health == (Object)null || ((NPC)Officer).Health.IsDead || (Object)(object)_pursuitTarget != (Object)null || (Object)(object)_currentSearchTarget != (Object)null) { return; } float value = K9Config.SniffRadius.Value; float num = value * Mathf.Max(1f, K9Config.ScentMaxRadiusMultiplier.Value); float num2 = num * num; float num3 = num2; Player val = null; float num4 = value; foreach (Player item in K9Manager.PlayerCache) { if ((Object)(object)item == (Object)null || (Object)(object)((Component)item).transform == (Object)null) { continue; } Vector3 val2 = ((Component)Officer).transform.position - ((Component)item).transform.position; float sqrMagnitude = ((Vector3)(ref val2)).sqrMagnitude; if (!(sqrMagnitude > num3) && HasDrugsCached(item)) { float num5 = K9ContrabandRegistry.SniffRadiusFactor(item); float num6 = value * num5; bool flag = IsOccludedFrom(item); if (flag) { num6 *= 0.35f; } bool flag2 = sqrMagnitude <= num6 * num6; LogDetection(item, Mathf.Sqrt(sqrMagnitude), num5, num6, flag, flag2); if (flag2) { num3 = sqrMagnitude; val = item; num4 = num6; } } } if ((Object)(object)val == (Object)null) { SetTracking(tracking: false); return; } int instanceID = ((Object)val).GetInstanceID(); float time = Time.time; if ((_lastSearchAt.TryGetValue(instanceID, out var value2) && time - value2 < K9Config.SearchCooldown.Value) || ((Object)(object)val.CrimeData != (Object)null && val.CrimeData.BodySearchPending)) { return; } _pursuitTarget = val; _pursuitAcquireRadius = Mathf.Max(0.5f, num4); if ((Object)(object)K9DogNPC != (Object)null) { K9DogNPC._pursuitTarget = _pursuitTarget; if (!_alertAnnounced) { _alertAnnounced = true; K9DogNPC.TriggerAlert(); } } SetTracking(tracking: true); EnsureOfficerAgentAndSetDestination(((Component)val).transform.position); NetState(K9NetSync.K9NetState.Tracking, _pursuitTarget, force: true); } private void SetTracking(bool tracking) { if ((Object)(object)Officer == (Object)null || (Object)(object)((NPC)Officer).Movement == (Object)null) { return; } try { if (tracking && !_speedBoostApplied) { _origMoveSpeedMultiplier = ((NPC)Officer).Movement.MoveSpeedMultiplier; float num = Mathf.Max(1f, K9Config.PursuitSpeedMultiplier.Value); ((NPC)Officer).Movement.MoveSpeedMultiplier = _origMoveSpeedMultiplier * num; _speedBoostApplied = true; } else if (!tracking && _speedBoostApplied) { ((NPC)Officer).Movement.MoveSpeedMultiplier = _origMoveSpeedMultiplier; _speedBoostApplied = false; } } catch { } } private void EnsureOfficerAgentAndSetDestination(Vector3 pos) { //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_006d: Unknown result type (might be due to invalid IL or missing references) try { Vector3 val = default(Vector3); if ((Object)(object)Officer != (Object)null && (Object)(object)((NPC)Officer).Movement != (Object)null && ((NPC)Officer).Movement.CanMove() && ((NPC)Officer).Movement.GetClosestReachablePoint(pos, ref val) && val != Vector3.zero) { ((NPC)Officer).Movement.SetDestination(val); } } catch { } } private void ClearPursuit(bool force = true) { _pursuitTarget = null; _currentSearchTarget = null; _searchObservedActive = false; _alertAnnounced = false; if ((Object)(object)K9DogNPC != (Object)null) { K9DogNPC._pursuitTarget = null; } SetTracking(tracking: false); RemoveSearchListeners(); NetState(K9NetSync.K9NetState.Following, null, force); } private void MaintainPursuit() { //IL_016f: Unknown result type (might be due to invalid IL or missing references) //IL_017f: Unknown result type (might be due to invalid IL or missing references) //IL_009e: Unknown result type (might be due to invalid IL or missing references) //IL_00ae: Unknown result type (might be due to invalid IL or missing references) //IL_00b3: Unknown result type (might be due to invalid IL or missing references) //IL_00b8: Unknown result type (might be due to invalid IL or missing references) //IL_0116: Unknown result type (might be due to invalid IL or missing references) //IL_011b: Unknown result type (might be due to invalid IL or missing references) //IL_0128: 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_0135: Unknown result type (might be due to invalid IL or missing references) //IL_0153: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)_pursuitTarget == (Object)null || (Object)(object)((Component)_pursuitTarget).transform == (Object)null) { ClearPursuit(force: false); return; } if (_isSearchListenerActive) { if ((Object)(object)Officer.BodySearchBehaviour != (Object)null && ((Behaviour)Officer.BodySearchBehaviour).Active && !((Object)(object)_currentSearchTarget != (Object)null)) { } return; } float num = _pursuitAcquireRadius * 1.35f; Vector3 val = ((Component)Officer).transform.position - ((Component)_pursuitTarget).transform.position; float sqrMagnitude = ((Vector3)(ref val)).sqrMagnitude; if (sqrMagnitude > num * num) { ClearPursuit(force: false); return; } try { if ((Object)(object)((NPC)Officer).Movement != (Object)null && ((NPC)Officer).Movement.CanMove()) { Vector3 position = ((Component)_pursuitTarget).transform.position; Vector3 val2 = default(Vector3); if (((NPC)Officer).Movement.GetClosestReachablePoint(position, ref val2) && val2 != Vector3.zero) { ((NPC)Officer).Movement.SetDestination(val2); } } } catch { } float num2 = Vector3.Distance(((Component)Officer).transform.position, ((Component)_pursuitTarget).transform.position); if (num2 <= 6f && num2 > BodySearchStartDistance && (Object)(object)_currentSearchTarget == (Object)null) { NetState(K9NetSync.K9NetState.Investigating, _pursuitTarget); } if (!(num2 <= BodySearchStartDistance)) { return; } int instanceID = ((Object)_pursuitTarget).GetInstanceID(); if (!_lastSearchAt.TryGetValue(instanceID, out var value) || Time.time - value >= K9Config.SearchCooldown.Value) { if ((!((Object)(object)Officer.BodySearchBehaviour != (Object)null) || !((Behaviour)Officer.BodySearchBehaviour).Active) && !(Time.time - _lastBeginSearchTime < 0.5f)) { _lastBeginSearchTime = Time.time; _currentSearchTarget = _pursuitTarget; NetState(K9NetSync.K9NetState.Searching, _pursuitTarget, force: true); Officer.BeginBodySearch_Networked(_pursuitTarget.PlayerCode); _lastSearchAt[((Object)_pursuitTarget).GetInstanceID()] = Time.time; Logger.Debug($"Body search REQUESTED by officer {((Object)Officer).GetInstanceID()} on {_pursuitTarget.PlayerCode}."); AddSearchListeners(); } } else { ClearPursuit(force: false); } } private void AddSearchListeners() { //IL_006e: Unknown result type (might be due to invalid IL or missing references) //IL_0078: Expected O, but got Unknown //IL_0088: Unknown result type (might be due to invalid IL or missing references) //IL_0092: Expected O, but got Unknown if (_isSearchListenerActive) { return; } BodySearchBehaviour val = (((Object)(object)Officer != (Object)null) ? Officer.BodySearchBehaviour : null); if ((Object)(object)val == (Object)null) { return; } _searchOutcome = SearchOutcome.Unknown; _searchObservedActive = false; _searchBeganAt = Time.time; _isSearchListenerActive = true; try { if (_onItemsFound == null) { _onItemsFound = new UnityAction(OnSearchFoundItems); } if (_onClear == null) { _onClear = new UnityAction(OnSearchClear); } UnityEvent onSearchComplete_ItemsFound = val.onSearchComplete_ItemsFound; if (onSearchComplete_ItemsFound != null) { onSearchComplete_ItemsFound.AddListener(_onItemsFound); } UnityEvent onSearchComplete_Clear = val.onSearchComplete_Clear; if (onSearchComplete_Clear != null) { onSearchComplete_Clear.AddListener(_onClear); } } catch (Exception ex) { Logger.Debug("K9UnitController: could not subscribe to body-search events (" + ex.Message + "); falling back to polling Active."); } } private void RemoveSearchListeners() { if (!_isSearchListenerActive) { return; } _isSearchListenerActive = false; try { BodySearchBehaviour val = (((Object)(object)Officer != (Object)null) ? Officer.BodySearchBehaviour : null); if (!((Object)(object)val != (Object)null)) { return; } if (_onItemsFound != null) { UnityEvent onSearchComplete_ItemsFound = val.onSearchComplete_ItemsFound; if (onSearchComplete_ItemsFound != null) { onSearchComplete_ItemsFound.RemoveListener(_onItemsFound); } } if (_onClear != null) { UnityEvent onSearchComplete_Clear = val.onSearchComplete_Clear; if (onSearchComplete_Clear != null) { onSearchComplete_Clear.RemoveListener(_onClear); } } } catch (Exception ex) { Logger.Debug("K9UnitController: could not unsubscribe from body-search events (" + ex.Message + ")."); } } private void OnSearchFoundItems() { _searchOutcome = SearchOutcome.ItemsFound; Logger.Debug("Body search concluded: items found."); } private void OnSearchClear() { _searchOutcome = SearchOutcome.Clear; Logger.Debug("Body search concluded: clear."); } private void HandleSearchComplete() { if ((Object)(object)_currentSearchTarget != (Object)null) { _lastSearchAt[((Object)_currentSearchTarget).GetInstanceID()] = Time.time; } Logger.Debug($"Search complete (outcome: {_searchOutcome})."); _searchOutcome = SearchOutcome.Unknown; ClearPursuit(); } private void FaceOfficerTowardTarget(Player target) { //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_0085: Unknown result type (might be due to invalid IL or missing references) //IL_0086: Unknown result type (might be due to invalid IL or missing references) //IL_008b: Unknown result type (might be due to invalid IL or missing references) //IL_00a2: Unknown result type (might be due to invalid IL or missing references) //IL_00a7: Unknown result type (might be due to invalid IL or missing references) //IL_00b3: Unknown result type (might be due to invalid IL or missing references) try { if (!((Object)(object)Officer == (Object)null) && !((Object)(object)((Component)Officer).transform == (Object)null) && !((Object)(object)target == (Object)null) && !((Object)(object)((Component)target).transform == (Object)null)) { Vector3 val = ((Component)target).transform.position - ((Component)Officer).transform.position; val.y = 0f; if (!(((Vector3)(ref val)).sqrMagnitude < 0.0004f)) { Quaternion val2 = Quaternion.LookRotation(val); ((Component)Officer).transform.rotation = Quaternion.Slerp(((Component)Officer).transform.rotation, val2, Time.deltaTime * 6f); } } } catch { } } private void FaceDogTowardTarget(Player target) { //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0037: 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_0064: 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) //IL_006a: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)K9DogNPC == (Object)null) && !((Object)(object)target == (Object)null)) { Transform transform = ((Component)K9DogNPC).transform; Vector3 val = ((Component)target).transform.position - transform.position; val.y = 0f; if (!(((Vector3)(ref val)).sqrMagnitude < 0.0004f)) { transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(val), Time.deltaTime * 6f); } } } private bool HasDrugsCached(Player p) { return K9ContrabandRegistry.HasContraband(p, Officer); } private void OnDestroy() { try { SetTracking(tracking: false); } catch { } try { RemoveSearchListeners(); } catch { } if (IsServer() && (Object)(object)Officer != (Object)null) { K9NetSync.ServerDespawn(Officer); } if ((Object)(object)Dog != (Object)null) { try { Object.Destroy((Object)(object)Dog); } catch { } } } } } namespace System.Runtime.CompilerServices { [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] internal sealed class IgnoresAccessChecksToAttribute : Attribute { internal IgnoresAccessChecksToAttribute(string assemblyName) { } } }