Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of FehuTameLimit v420.101.2
BepInEx/plugins/FehuTameLimit/FehuTameLimit.dll
Decompiled a week agousing System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Serialization; using System.Runtime.Serialization.Json; using System.Runtime.Versioning; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: AssemblyCompany("FehuTameLimit")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("420.101.1.0")] [assembly: AssemblyInformationalVersion("420.101.1+FEHU_TAME_RELEASE_20260922")] [assembly: AssemblyProduct("FehuTameLimit")] [assembly: AssemblyTitle("FehuTameLimit")] [assembly: AssemblyVersion("420.101.1.0")] namespace Forteca.FehuTameLimit; internal sealed class PendingBirth { internal Vector3 Position; internal HashSet<string> BeforeKeys; internal long OwnerId; internal string OwnerName; internal string ClanName; internal float Expires; } internal static class BreedingService { private static readonly List<PendingBirth> Pending = new List<PendingBirth>(); private static float _nextSearch; internal static HashSet<string> CaptureNearbyKeys(Vector3 pos, float radius) { //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_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) HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal); float num = radius * radius; Character[] array = Object.FindObjectsByType<Character>((FindObjectsSortMode)0); foreach (Character val in array) { if ((Object)(object)val == (Object)null) { continue; } Vector3 val2 = ((Component)val).transform.position - pos; if (((Vector3)(ref val2)).sqrMagnitude > num) { continue; } ZNetView component = ((Component)val).GetComponent<ZNetView>(); if (!((Object)(object)component == (Object)null) && component.IsValid()) { string stableZdoKey = TameOwnership.GetStableZdoKey(component.GetZDO()); if (!string.IsNullOrEmpty(stableZdoKey)) { hashSet.Add(stableZdoKey); } } } return hashSet; } internal static void Add(PendingBirth birth) { if (birth != null) { Pending.Add(birth); } } internal static void Clear() { Pending.Clear(); _nextSearch = 0f; } internal static void UpdatePending() { if (!FehuTameLimitPlugin.IsServer() || Pending.Count == 0) { return; } float realtimeSinceStartup = Time.realtimeSinceStartup; if (realtimeSinceStartup < _nextSearch) { return; } _nextSearch = realtimeSinceStartup + 0.25f; Character[] candidates = Object.FindObjectsByType<Character>((FindObjectsSortMode)0); for (int num = Pending.Count - 1; num >= 0; num--) { PendingBirth pendingBirth = Pending[num]; if (TryResolveChild(pendingBirth, candidates) || realtimeSinceStartup >= pendingBirth.Expires) { Pending.RemoveAt(num); } } } private static bool TryResolveChild(PendingBirth birth, Character[] candidates) { //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) float num = 144f; foreach (Character val in candidates) { if ((Object)(object)val == (Object)null) { continue; } Vector3 val2 = ((Component)val).transform.position - birth.Position; if (((Vector3)(ref val2)).sqrMagnitude > num) { continue; } GameObject gameObject = ((Component)val).gameObject; if (!TameOwnership.IsTameCandidate(gameObject)) { continue; } ZNetView component = gameObject.GetComponent<ZNetView>(); if ((Object)(object)component == (Object)null || !component.IsValid()) { continue; } string stableZdoKey = TameOwnership.GetStableZdoKey(component.GetZDO()); if (string.IsNullOrEmpty(stableZdoKey) || birth.BeforeKeys.Contains(stableZdoKey)) { continue; } if (TameOwnership.TryRead(gameObject, out var identity) && identity.HasOwner) { if (identity.OwnerId == birth.OwnerId) { TameOwnership.WriteState(identity.Zdo, TameRuntimeState.OwnedInactive); identity.State = TameRuntimeState.OwnedInactive; identity.HasExplicitState = true; TameRegistry.Upsert(identity); TameRegistry.FlushIfDirty(); return true; } continue; } OwnershipService.TryAssign(gameObject, birth.OwnerId, birth.OwnerName, birth.ClanName, "breeding", preferRoster: false); if (TameOwnership.TryRead(gameObject, out var identity2)) { TameOwnership.WriteState(identity2.Zdo, TameRuntimeState.OwnedInactive); identity2.State = TameRuntimeState.OwnedInactive; identity2.HasExplicitState = true; TameRegistry.Upsert(identity2); } TameRegistry.FlushIfDirty(); return true; } return false; } } internal static class ClanBridge { private sealed class CacheEntry { internal string Clan; internal bool ClanResolved; internal bool Supporter; internal bool SupporterResolved; internal float Time; } private static readonly Dictionary<long, CacheEntry> Cache = new Dictionary<long, CacheEntry>(); private const float CacheSeconds = 15f; private static bool _fcwTypeResolved; private static Type _fcwType; private static bool _guildsTypeResolved; private static Type _guildsType; internal static void Reset() { Cache.Clear(); } internal static bool TryResolve(long ownerId, string ownerName, out string clan) { CacheEntry cacheEntry = Resolve(ownerId, ownerName); clan = cacheEntry.Clan ?? string.Empty; return cacheEntry.ClanResolved; } internal static bool IsSupporter(long ownerId, string ownerName) { CacheEntry cacheEntry = Resolve(ownerId, ownerName); if (cacheEntry.SupporterResolved) { return cacheEntry.Supporter; } return false; } private static CacheEntry Resolve(long ownerId, string ownerName) { float realtimeSinceStartup = Time.realtimeSinceStartup; if (ownerId != 0L && Cache.TryGetValue(ownerId, out var value) && realtimeSinceStartup - value.Time < 15f) { return value; } CacheEntry cacheEntry = new CacheEntry { Clan = string.Empty, Time = realtimeSinceStartup }; if (ownerId != 0L && TryResolveFcw(ownerId, out var clan, out var supporter, out var supporterResolved)) { cacheEntry.Clan = TameOwnership.NormalizeClan(clan); cacheEntry.ClanResolved = true; cacheEntry.Supporter = supporter; cacheEntry.SupporterResolved = supporterResolved; } else { cacheEntry.ClanResolved = TryResolveGuilds(ownerId, ownerName, out var clan2); cacheEntry.Clan = TameOwnership.NormalizeClan(clan2); } if (ownerId != 0L) { Cache[ownerId] = cacheEntry; } return cacheEntry; } private static bool TryResolveFcw(long ownerId, out string clan, out bool supporter, out bool supporterResolved) { clan = string.Empty; supporter = false; supporterResolved = false; Type type = ResolveOptionalType("FortecaClanWarfare.Core.WarfareManager", ref _fcwTypeResolved, ref _fcwType); if (type == null) { return false; } object obj = GetStaticMember(type, "Instance") ?? GetStaticMember(type, "instance"); if (obj == null) { return false; } object instanceMember = GetInstanceMember(obj, "Data"); if (instanceMember == null) { return false; } object obj2 = InvokeSingleArg(instanceMember, "GetPlayer", ownerId); if (obj2 == null) { return false; } long num = ToInt64(GetInstanceMember(obj2, "ClanId")); if (num != 0L) { object obj3 = InvokeSingleArg(instanceMember, "GetClan", num); clan = ((obj3 != null) ? GetInstanceMember(obj3, "Name") : null)?.ToString() ?? num.ToString(); } object instanceMember2 = GetInstanceMember(obj2, "SteamId64"); if (instanceMember2 != null) { object obj4 = InvokeSingleArg(obj, "GetSupporterBadges", instanceMember2); if (obj4 != null) { supporterResolved = true; supporter = ContainsSupporterBadge(obj4); } } return true; } private static bool ContainsSupporterBadge(object badges) { if (badges == null) { return false; } if (badges is string text) { if (!string.Equals(text.Trim(), "Supporter", StringComparison.OrdinalIgnoreCase)) { return text.IndexOf("Supporter", StringComparison.OrdinalIgnoreCase) >= 0; } return true; } if (badges is IEnumerable enumerable) { foreach (object item in enumerable) { if (item != null) { if (string.Equals(item.ToString(), "Supporter", StringComparison.OrdinalIgnoreCase)) { return true; } object obj = GetInstanceMember(item, "Name") ?? GetInstanceMember(item, "Id") ?? GetInstanceMember(item, "Key"); if (obj != null && string.Equals(obj.ToString(), "Supporter", StringComparison.OrdinalIgnoreCase)) { return true; } } } } return false; } private static bool TryResolveGuilds(long ownerId, string ownerName, out string clan) { clan = string.Empty; Type type = ResolveOptionalType("Guilds.API", ref _guildsTypeResolved, ref _guildsType); if (type == null) { return false; } string[] array = new string[3] { "GetPlayerGuild", "GetGuildForPlayer", "GetPlayerGuildName" }; foreach (string name in array) { foreach (MethodInfo item in from m in type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) where m.Name == name select m) { ParameterInfo[] parameters = item.GetParameters(); if (parameters.Length != 1) { continue; } object obj = ConvertArg(parameters[0].ParameterType, ownerId, ownerName); if (obj == null) { continue; } try { object obj2 = item.Invoke(null, new object[1] { obj }); if (TryExtractClan(obj2, out clan)) { return true; } if (obj2 == null) { return true; } } catch { } } } return false; } private static Type ResolveOptionalType(string fullName, ref bool resolved, ref Type cached) { if (resolved) { return cached; } resolved = true; Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { try { Type type = assembly.GetType(fullName, throwOnError: false); if (type != null) { cached = type; break; } } catch { } } return cached; } private static object InvokeSingleArg(object target, string name, object value) { if (target == null) { return null; } foreach (MethodInfo item in from m in target.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) where m.Name == name select m) { ParameterInfo[] parameters = item.GetParameters(); if (parameters.Length != 1) { continue; } object obj = ConvertArg(parameters[0].ParameterType, value); if (obj != null || value == null || !parameters[0].ParameterType.IsValueType) { try { return item.Invoke(target, new object[1] { obj }); } catch { } } } return null; } private static object ConvertArg(Type targetType, object value) { if (value == null) { return null; } if (targetType.IsInstanceOfType(value)) { return value; } try { if (targetType == typeof(string)) { return value.ToString(); } if (targetType == typeof(long)) { return Convert.ToInt64(value); } if (targetType == typeof(ulong)) { return Convert.ToUInt64(value); } if (targetType == typeof(int)) { return Convert.ToInt32(value); } return Convert.ChangeType(value, targetType); } catch { return null; } } private static object ConvertArg(Type targetType, long ownerId, string ownerName) { if (targetType == typeof(string)) { return ownerName ?? string.Empty; } return ConvertArg(targetType, ownerId); } private static object GetStaticMember(Type type, string name) { try { PropertyInfo property = type.GetProperty(name, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (property != null) { return property.GetValue(null, null); } return type.GetField(name, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(null); } catch { return null; } } private static object GetInstanceMember(object target, string name) { if (target == null) { return null; } try { Type type = target.GetType(); PropertyInfo property = type.GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (property != null) { return property.GetValue(target, null); } return type.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(target); } catch { return null; } } private static bool TryExtractClan(object value, out string clan) { clan = string.Empty; if (value == null) { return false; } if (value is string text) { clan = text; return true; } string[] array = new string[7] { "ClanName", "Name", "Tag", "ClanId", "ClanID", "Id", "ID" }; foreach (string name in array) { object instanceMember = GetInstanceMember(value, name); if (instanceMember != null) { clan = instanceMember.ToString(); return true; } } return false; } private static long ToInt64(object value) { try { return (value == null) ? 0 : Convert.ToInt64(value); } catch { return 0L; } } } internal sealed class FehuConfig { internal readonly ConfigEntry<bool> Enabled; internal readonly ConfigEntry<bool> EnforceRosterLimit; internal readonly ConfigEntry<int> NormalPlayerRoster; internal readonly ConfigEntry<bool> EnforceActiveLimit; internal readonly ConfigEntry<int> NormalPlayerActive; internal readonly ConfigEntry<int> SupporterActive; internal readonly ConfigEntry<bool> EnforceClanActiveLimit; internal readonly ConfigEntry<int> ClanActive; internal readonly ConfigEntry<bool> NewTamesEnterRoster; internal readonly ConfigEntry<float> OwnershipAssignRadius; internal readonly ConfigEntry<bool> AdoptOnTamedInteract; internal readonly ConfigEntry<bool> GuardWardCountsAsTameGuard; internal readonly ConfigEntry<bool> BootstrapGlobalZdoScan; internal readonly ConfigEntry<float> ReconcileSeconds; internal readonly ConfigEntry<float> ClanRefreshSeconds; internal readonly ConfigEntry<float> SnapshotSeconds; internal readonly ConfigEntry<bool> DebugLogging; internal int RosterLimit => NonNegativeOrDefault(NormalPlayerRoster.Value, 6); internal int NormalActiveLimit => NonNegativeOrDefault(NormalPlayerActive.Value, 3); internal int SupporterActiveLimit => NonNegativeOrDefault(SupporterActive.Value, 6); internal int ClanActiveLimit => NonNegativeOrDefault(ClanActive.Value, 50); private static int NonNegativeOrDefault(int value, int fallback) { if (value >= 0) { return value; } return fallback; } internal static float PositiveOrDefault(float value, float minimum, float fallback) { if (float.IsNaN(value) || float.IsInfinity(value)) { return fallback; } return Math.Max(minimum, value); } internal FehuConfig(ConfigFile config) { Enabled = config.Bind<bool>("General", "Enabled", true, "Master switch."); EnforceRosterLimit = config.Bind<bool>("Limits", "EnforceRosterLimit", true, "Limit how many non-guard tames may be in a player's roster at once."); NormalPlayerRoster = config.Bind<int>("Limits", "NormalPlayerRoster", 6, "Maximum roster size per player. Ownership outside the roster remains unlimited. 0 means unlimited; negative values use the default."); EnforceActiveLimit = config.Bind<bool>("Limits", "EnforceActiveLimit", true, "Limit released/active tames per player."); NormalPlayerActive = config.Bind<int>("Limits", "NormalPlayerActive", 3, "Maximum released/active tames for a normal player. 0 means unlimited; negative values use the default."); SupporterActive = config.Bind<int>("Limits", "SupporterActive", 6, "Maximum released/active tames for a Supporter. 0 means unlimited; negative values use the default."); EnforceClanActiveLimit = config.Bind<bool>("Limits", "EnforceClanActiveLimit", true, "Limit released/active tames across an FCW clan."); ClanActive = config.Bind<int>("Limits", "ClanActive", 50, "Maximum released/active non-guard tames across an FCW clan. TameGuards are excluded. 0 means unlimited; negative values use the default."); NewTamesEnterRoster = config.Bind<bool>("State", "NewTamesEnterRoster", true, "Newly tamed/adopted creatures enter the roster when a roster slot exists; otherwise they remain owned/inactive."); OwnershipAssignRadius = config.Bind<float>("Ownership", "OwnershipAssignRadius", 100f, "Maximum radius used to attribute a newly tamed/legacy breeding animal to the nearest online player."); AdoptOnTamedInteract = config.Bind<bool>("Ownership", "AdoptOnTamedInteract", true, "When server-side interaction is observed, stamp an unowned legacy tame to that player without limiting total ownership."); GuardWardCountsAsTameGuard = config.Bind<bool>("Compatibility", "GuardWardCountsAsTameGuard", true, "Treat TGW GuardWard stance and Fehu TCW guard metadata as TameGuard state, excluded from roster/active/clan caps."); BootstrapGlobalZdoScan = config.Bind<bool>("Persistence", "BootstrapGlobalZdoScan", true, "One-time incremental ZDO bootstrap for FTL/TGW-owned animals after world load."); ReconcileSeconds = config.Bind<float>("Performance", "ReconcileSeconds", 20f, "Loaded tame state reconciliation interval."); ClanRefreshSeconds = config.Bind<float>("Performance", "ClanRefreshSeconds", 30f, "How often stored FCW clan assignments are refreshed."); SnapshotSeconds = config.Bind<float>("Performance", "SnapshotSeconds", 2f, "Authoritative roster/active count snapshot interval to modded clients."); DebugLogging = config.Bind<bool>("Debug", "DebugLogging", false, "Verbose diagnostic logging."); } } internal readonly struct LimitDecision { internal readonly bool Allowed; internal readonly string Message; internal readonly int RosterCount; internal readonly int RosterLimit; internal readonly int ActiveCount; internal readonly int ActiveLimit; internal readonly int ClanActiveCount; internal readonly int ClanActiveLimit; internal readonly bool Supporter; internal LimitDecision(bool allowed, string message, int rosterCount, int rosterLimit, int activeCount, int activeLimit, int clanActiveCount, int clanActiveLimit, bool supporter) { Allowed = allowed; Message = message; RosterCount = rosterCount; RosterLimit = rosterLimit; ActiveCount = activeCount; ActiveLimit = activeLimit; ClanActiveCount = clanActiveCount; ClanActiveLimit = clanActiveLimit; Supporter = supporter; } } internal static class LimitService { internal static LimitDecision CanTransition(in TameIdentity identity, TameRuntimeState target) { int rosterLimit = FehuTameLimitPlugin.Settings.RosterLimit; bool flag = ClanBridge.IsSupporter(identity.OwnerId, identity.OwnerName); int num = (flag ? FehuTameLimitPlugin.Settings.SupporterActiveLimit : FehuTameLimitPlugin.Settings.NormalActiveLimit); int clanActiveLimit = FehuTameLimitPlugin.Settings.ClanActiveLimit; int num2 = TameRegistry.CountPlayerRoster(identity.OwnerId, identity.Key); int num3 = TameRegistry.CountPlayerActive(identity.OwnerId, identity.Key); int num4 = (IsRealClan(identity.ClanName) ? TameRegistry.CountClanActive(identity.ClanName, identity.Key) : 0); if (!FehuTameLimitPlugin.Settings.Enabled.Value || target == identity.State || target == TameRuntimeState.OwnedInactive || target == TameRuntimeState.TameGuard) { return new LimitDecision(allowed: true, string.Empty, num2, rosterLimit, num3, num, num4, clanActiveLimit, flag); } bool flag2 = identity.State == TameRuntimeState.Roster || identity.State == TameRuntimeState.Active; if ((target == TameRuntimeState.Roster || target == TameRuntimeState.Active) && !flag2 && FehuTameLimitPlugin.Settings.EnforceRosterLimit.Value && rosterLimit > 0 && num2 >= rosterLimit) { return new LimitDecision(allowed: false, $"Tame roster full: {num2}/{rosterLimit}", num2, rosterLimit, num3, num, num4, clanActiveLimit, flag); } if (target == TameRuntimeState.Active) { if (FehuTameLimitPlugin.Settings.EnforceActiveLimit.Value && num > 0 && num3 >= num) { return new LimitDecision(allowed: false, string.Format("Active tame limit reached: {0}/{1}{2}", num3, num, flag ? " (Supporter)" : string.Empty), num2, rosterLimit, num3, num, num4, clanActiveLimit, flag); } if (FehuTameLimitPlugin.Settings.EnforceClanActiveLimit.Value && clanActiveLimit > 0 && IsRealClan(identity.ClanName) && num4 >= clanActiveLimit) { return new LimitDecision(allowed: false, $"Clan active tame limit reached: {num4}/{clanActiveLimit}", num2, rosterLimit, num3, num, num4, clanActiveLimit, flag); } } return new LimitDecision(allowed: true, string.Empty, num2, rosterLimit, num3, num, num4, clanActiveLimit, flag); } internal static bool IsRealClan(string clan) { return !string.IsNullOrWhiteSpace(TameOwnership.NormalizeClan(clan)); } } internal static class NetworkSync { private const string RpcSnapshot = "FehuTameLimit_Snapshot_v3"; private const string RpcNotice = "FehuTameLimit_Notice_v3"; private static ZRoutedRpc _registeredRpc; private static bool _haveSnapshot; private static int _lastSnapshotRevision = -1; private static float _nextHeartbeat; private static readonly Dictionary<long, int> PlayerRoster = new Dictionary<long, int>(); private static readonly Dictionary<long, int> PlayerActive = new Dictionary<long, int>(); private static readonly Dictionary<string, int> ClanActive = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase); internal static bool HaveSnapshot => _haveSnapshot; internal static int ServerRosterLimit { get; private set; } = 6; internal static int ServerNormalActiveLimit { get; private set; } = 3; internal static int ServerSupporterActiveLimit { get; private set; } = 6; internal static int ServerClanActiveLimit { get; private set; } = 50; internal static void TryRegister() { if (ZRoutedRpc.instance == null || _registeredRpc == ZRoutedRpc.instance) { return; } try { ZRoutedRpc.instance.Register<ZPackage>("FehuTameLimit_Snapshot_v3", (Action<long, ZPackage>)OnSnapshot); ZRoutedRpc.instance.Register<ZPackage>("FehuTameLimit_Notice_v3", (Action<long, ZPackage>)OnNotice); _registeredRpc = ZRoutedRpc.instance; } catch (Exception ex) { if (FehuTameLimitPlugin.Settings.DebugLogging.Value) { FehuTameLimitPlugin.Log.LogDebug((object)("RPC registration deferred: " + ex.Message)); } } } internal static void BroadcastSnapshot() { //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Expected O, but got Unknown if (!FehuTameLimitPlugin.IsServer() || ZRoutedRpc.instance == null) { return; } int revision = TameRegistry.Revision; float realtimeSinceStartup = Time.realtimeSinceStartup; if (revision == _lastSnapshotRevision && realtimeSinceStartup < _nextHeartbeat) { return; } ZPackage val = new ZPackage(); val.Write(3); val.Write(FehuTameLimitPlugin.Settings.RosterLimit); val.Write(FehuTameLimitPlugin.Settings.NormalActiveLimit); val.Write(FehuTameLimitPlugin.Settings.SupporterActiveLimit); val.Write(FehuTameLimitPlugin.Settings.ClanActiveLimit); Dictionary<long, int> playerRosterCounts = TameRegistry.GetPlayerRosterCounts(); val.Write(playerRosterCounts.Count); foreach (KeyValuePair<long, int> item in playerRosterCounts) { val.Write(item.Key); val.Write(item.Value); } Dictionary<long, int> playerActiveCounts = TameRegistry.GetPlayerActiveCounts(); val.Write(playerActiveCounts.Count); foreach (KeyValuePair<long, int> item2 in playerActiveCounts) { val.Write(item2.Key); val.Write(item2.Value); } Dictionary<string, int> clanActiveCounts = TameRegistry.GetClanActiveCounts(); val.Write(clanActiveCounts.Count); foreach (KeyValuePair<string, int> item3 in clanActiveCounts) { val.Write(item3.Key ?? string.Empty); val.Write(item3.Value); } ZRoutedRpc.instance.InvokeRoutedRPC(0L, "FehuTameLimit_Snapshot_v3", new object[1] { val }); _lastSnapshotRevision = revision; _nextHeartbeat = realtimeSinceStartup + 10f; } internal static void SendNotice(long ownerId, string message) { //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Expected O, but got Unknown if (FehuTameLimitPlugin.IsServer() && ZRoutedRpc.instance != null && ownerId != 0L && !string.IsNullOrEmpty(message)) { ZPackage val = new ZPackage(); val.Write(ownerId); val.Write(message); ZRoutedRpc.instance.InvokeRoutedRPC(0L, "FehuTameLimit_Notice_v3", new object[1] { val }); } } private static void OnSnapshot(long sender, ZPackage pkg) { if (!IsFromServer(sender)) { return; } try { if (pkg.ReadInt() == 3) { ServerRosterLimit = pkg.ReadInt(); ServerNormalActiveLimit = pkg.ReadInt(); ServerSupporterActiveLimit = pkg.ReadInt(); ServerClanActiveLimit = pkg.ReadInt(); PlayerRoster.Clear(); int num = pkg.ReadInt(); for (int i = 0; i < num; i++) { PlayerRoster[pkg.ReadLong()] = pkg.ReadInt(); } PlayerActive.Clear(); int num2 = pkg.ReadInt(); for (int j = 0; j < num2; j++) { PlayerActive[pkg.ReadLong()] = pkg.ReadInt(); } ClanActive.Clear(); int num3 = pkg.ReadInt(); for (int k = 0; k < num3; k++) { ClanActive[pkg.ReadString()] = pkg.ReadInt(); } _haveSnapshot = true; } } catch (Exception ex) { FehuTameLimitPlugin.Log.LogWarning((object)("Invalid tame-limit snapshot: " + ex.Message)); } } private static void OnNotice(long sender, ZPackage pkg) { if (!IsFromServer(sender)) { return; } try { long num = pkg.ReadLong(); string text = pkg.ReadString(); Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer != (Object)null && localPlayer.GetPlayerID() == num) { ((Character)localPlayer).Message((MessageType)2, text, 0, (Sprite)null, false); } } catch { } } internal static int GetPlayerRoster(long id) { if (!PlayerRoster.TryGetValue(id, out var value)) { return 0; } return value; } internal static int GetPlayerActive(long id) { if (!PlayerActive.TryGetValue(id, out var value)) { return 0; } return value; } internal static int GetClanActive(string clan) { if (!ClanActive.TryGetValue(clan ?? string.Empty, out var value)) { return 0; } return value; } internal static void ClearClientCache() { PlayerRoster.Clear(); PlayerActive.Clear(); ClanActive.Clear(); _haveSnapshot = false; } private static bool IsFromServer(long sender) { ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null) { return false; } if (instance.IsServer()) { return sender == ZNet.GetUID(); } ZNetPeer serverPeer = instance.GetServerPeer(); if (serverPeer != null) { return sender == serverPeer.m_uid; } return false; } } internal static class OwnershipService { internal static Player FindNearestPlayer(Vector3 position, float radius) { //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_003b: 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) Player result = null; float num = radius * radius; Player[] array = Object.FindObjectsByType<Player>((FindObjectsSortMode)0); foreach (Player val in array) { if (!((Object)(object)val == (Object)null) && ((Component)val).gameObject.activeInHierarchy) { Vector3 val2 = ((Component)val).transform.position - position; float sqrMagnitude = ((Vector3)(ref val2)).sqrMagnitude; if (sqrMagnitude <= num) { num = sqrMagnitude; result = val; } } } return result; } internal static string ResolveClan(long ownerId, string ownerName, string fallback) { if (ClanBridge.TryResolve(ownerId, ownerName, out var clan)) { return clan; } return TameOwnership.NormalizeClan(fallback); } internal static bool TryAssign(GameObject go, Player player, string source, bool preferRoster) { if ((Object)(object)go == (Object)null || (Object)(object)player == (Object)null) { return false; } string ownerName = SafeName(player); return TryAssign(go, player.GetPlayerID(), ownerName, ResolveClan(player.GetPlayerID(), ownerName, string.Empty), source, preferRoster); } internal static bool TryAssign(GameObject go, long ownerId, string ownerName, string clan, string source, bool preferRoster) { if ((Object)(object)go == (Object)null || ownerId == 0L) { return false; } ZNetView component = go.GetComponent<ZNetView>(); if ((Object)(object)component == (Object)null || !component.IsValid() || component.GetZDO() == null) { return false; } string clan2 = ResolveClan(ownerId, ownerName, clan); if (TameOwnership.TryRead(go, out var identity) && identity.OwnerId == ownerId) { TameOwnership.WriteOwnershipOnly(component.GetZDO(), ownerId, ownerName, clan2, source); if (!identity.HasExplicitState) { TameOwnership.WriteState(component.GetZDO(), TameStateService.ChooseInitialState(go, ownerId, ownerName, clan2, preferRoster)); } if (TameOwnership.TryRead(go, out var identity2)) { TameRegistry.Upsert(identity2); } return true; } TameRuntimeState state = TameStateService.ChooseInitialState(go, ownerId, ownerName, clan2, preferRoster); TameOwnership.WriteFtl(component.GetZDO(), ownerId, ownerName, clan2, source, state); if (TameOwnership.TryRead(go, out var identity3)) { TameRegistry.Upsert(identity3); } return true; } internal static bool TryEnsureBreedingOwner(GameObject go, out TameIdentity identity) { //IL_0066: Unknown result type (might be due to invalid IL or missing references) if (TameOwnership.TryRead(go, out identity) && identity.HasOwner) { string text = ResolveClan(identity.OwnerId, identity.OwnerName, identity.ClanName); if (!string.Equals(text, identity.ClanName, StringComparison.OrdinalIgnoreCase) && identity.Zdo != null) { TameOwnership.UpdateClan(identity.Zdo, text); identity.ClanName = text; TameRegistry.Upsert(identity); } return true; } Player val = FindNearestPlayer(go.transform.position, FehuConfig.PositiveOrDefault(FehuTameLimitPlugin.Settings.OwnershipAssignRadius.Value, 1f, 100f)); if ((Object)(object)val == (Object)null) { identity = default(TameIdentity); return false; } if (!TryAssign(go, val, "legacy-breeding", preferRoster: false)) { identity = default(TameIdentity); return false; } return TameOwnership.TryRead(go, out identity); } internal static string SafeName(Player player) { try { return player.GetPlayerName() ?? string.Empty; } catch { return string.Empty; } } } [HarmonyPatch(typeof(Procreation), "Procreate")] internal static class ProcreationOwnershipPatch { private sealed class State { internal PendingBirth Birth; } private static void Prefix(Procreation __instance, ref object __state) { //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) __state = null; if (FehuTameLimitPlugin.IsServer() && FehuTameLimitPlugin.Settings.Enabled.Value && !((Object)(object)__instance == (Object)null)) { GameObject gameObject = ((Component)__instance).gameObject; if (OwnershipService.TryEnsureBreedingOwner(gameObject, out var identity)) { __state = new State { Birth = new PendingBirth { Position = gameObject.transform.position, BeforeKeys = BreedingService.CaptureNearbyKeys(gameObject.transform.position, 12f), OwnerId = identity.OwnerId, OwnerName = identity.OwnerName, ClanName = identity.ClanName, Expires = Time.realtimeSinceStartup + 8f } }; } } } private static void Postfix(object __state) { State state = __state as State; if (state?.Birth != null) { BreedingService.Add(state.Birth); } } } [HarmonyPatch(typeof(Character), "OnDeath")] internal static class CharacterDeathPatch { private static void Prefix(Character __instance) { if (FehuTameLimitPlugin.IsServer() && !((Object)(object)__instance == (Object)null)) { TameRegistry.Remove(((Component)__instance).gameObject); TameRegistry.FlushIfDirty(); } } } [HarmonyPatch(typeof(Game), "Start")] internal static class GameStartPatch { private static void Postfix() { NetworkSync.TryRegister(); } } [BepInPlugin("com.forteca.fehutamelimit", "Fehu Tame Limit", "420.101.1")] public sealed class FehuTameLimitPlugin : BaseUnityPlugin { public const string PluginGuid = "com.forteca.fehutamelimit"; public const string PluginName = "Fehu Tame Limit"; public const string PluginVersion = "420.101.1"; public const string BuildMarker = "FEHU_TAME_RELEASE_20260922"; internal static FehuTameLimitPlugin Instance; internal static ManualLogSource Log; internal static FehuConfig Settings; private Harmony _harmony; private long _worldUid = long.MinValue; private float _nextReconcile; private float _nextClanRefresh; private float _nextSnapshot; private void Awake() { //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Expected O, but got Unknown Instance = this; Log = ((BaseUnityPlugin)this).Logger; Settings = new FehuConfig(((BaseUnityPlugin)this).Config); _harmony = new Harmony("com.forteca.fehutamelimit"); _harmony.PatchAll(typeof(FehuTameLimitPlugin).Assembly); VanillaIntegration.TryInstall(_harmony); Log.LogInfo((object)string.Format("{0} {1} [{2}] loaded. Ownership=unlimited, Roster={3}, Active={4}, SupporterActive={5}, ClanActive={6}, TameGuards excluded.", "Fehu Tame Limit", "420.101.1", "FEHU_TAME_RELEASE_20260922", Settings.RosterLimit, Settings.NormalActiveLimit, Settings.SupporterActiveLimit, Settings.ClanActiveLimit)); } private IEnumerator Start() { yield return null; TgwIntegration.InitializeOnce(_harmony); } private void Update() { if (Settings == null || !Settings.Enabled.Value) { return; } NetworkSync.TryRegister(); BreedingService.UpdatePending(); if (IsServer() && EnsureWorld()) { float realtimeSinceStartup = Time.realtimeSinceStartup; if (realtimeSinceStartup >= _nextReconcile) { _nextReconcile = realtimeSinceStartup + FehuConfig.PositiveOrDefault(Settings.ReconcileSeconds.Value, 2f, 20f); TameRegistry.ReconcileLoadedCharacters(); TameRegistry.FlushIfDirty(); } if (realtimeSinceStartup >= _nextClanRefresh) { _nextClanRefresh = realtimeSinceStartup + FehuConfig.PositiveOrDefault(Settings.ClanRefreshSeconds.Value, 5f, 30f); ClanBridge.Reset(); TameRegistry.RefreshClanAssignments(); } if (realtimeSinceStartup >= _nextSnapshot) { _nextSnapshot = realtimeSinceStartup + FehuConfig.PositiveOrDefault(Settings.SnapshotSeconds.Value, 0.5f, 2f); NetworkSync.BroadcastSnapshot(); } } } private bool EnsureWorld() { if ((Object)(object)ZNet.instance == (Object)null) { return false; } long worldUID; try { worldUID = ZNet.instance.GetWorldUID(); } catch { return false; } if (worldUID == 0L) { return false; } if (_worldUid == worldUID) { return true; } _worldUid = worldUID; BreedingService.Clear(); TameRegistry.LoadWorld(worldUID, SafeWorldName()); NetworkSync.ClearClientCache(); ClanBridge.Reset(); if (Settings.BootstrapGlobalZdoScan.Value) { ((MonoBehaviour)this).StartCoroutine(TameRegistry.BootstrapWorldZdos()); } else { TameRegistry.ReconcileLoadedCharacters(); } return true; } internal static bool IsServer() { if ((Object)(object)ZNet.instance != (Object)null) { return ZNet.instance.IsServer(); } return false; } internal static string SafeWorldName() { try { return ((Object)(object)ZNet.instance != (Object)null) ? (ZNet.instance.GetWorldName() ?? "world") : "world"; } catch { return "world"; } } private void OnDestroy() { try { if (IsServer()) { TameRegistry.Flush(force: true); } } catch { } Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } } } internal enum OwnershipSource { None, FehuTameLimit, TameGuildWars } internal enum TameRuntimeState { OwnedInactive, Roster, Active, TameGuard } internal struct TameIdentity { internal string Key; internal long OwnerId; internal string OwnerName; internal string ClanName; internal string PrefabName; internal OwnershipSource Source; internal TameRuntimeState State; internal bool HasExplicitState; internal ZDO Zdo; internal GameObject GameObject; internal bool HasOwner => OwnerId != 0; } internal static class TameOwnership { internal const string FtlOwnerId = "FTL_OwnerID"; internal const string FtlOwnerName = "FTL_OwnerName"; internal const string FtlClan = "FTL_Clan"; internal const string FtlState = "FTL_State"; internal const string FtlActive = "FTL_Active"; internal const string FtlSource = "FTL_Source"; internal const string TgwOwnerId = "TGW_TamerID"; internal const string TgwOwnerName = "TGW_TamerName"; internal const string TgwClan = "TGW_GuildName"; internal const string TgwObedience = "TGW_Obedience"; internal const string TgwStance = "TGW_Stance"; internal const string TcwClanId = "Fehu_TCW_ClanId"; internal const string TcwPatrolMode = "Fehu_TCW_PatrolMode"; internal const string TcwPatrolHomeX = "Fehu_TCW_PatrolHomeX"; internal const string TcwPatrolHomeY = "Fehu_TCW_PatrolHomeY"; private const int MissingState = -987654321; private static readonly FieldInfo ZdoUidField = AccessTools.Field(typeof(ZDO), "m_uid"); internal static bool TryRead(GameObject go, out TameIdentity identity) { identity = default(TameIdentity); if ((Object)(object)go == (Object)null) { return false; } ZNetView component = go.GetComponent<ZNetView>(); if ((Object)(object)component == (Object)null || !component.IsValid()) { return false; } if (!TryRead(component.GetZDO(), out identity)) { return false; } identity.GameObject = go; identity.PrefabName = NormalizePrefabName(((Object)go).name); return true; } internal static bool TryRead(ZDO zdo, out TameIdentity identity) { identity = default(TameIdentity); if (zdo == null) { return false; } long num = zdo.GetLong("FTL_OwnerID", 0L); long num2 = zdo.GetLong("TGW_TamerID", 0L); string a = zdo.GetString("FTL_Source", string.Empty); OwnershipSource ownershipSource = ((num == 0L) ? ((num2 != 0L) ? OwnershipSource.TameGuildWars : OwnershipSource.None) : ((!string.Equals(a, "TGW", StringComparison.OrdinalIgnoreCase)) ? OwnershipSource.FehuTameLimit : OwnershipSource.TameGuildWars)); if (ownershipSource == OwnershipSource.None) { return false; } long ownerId = ((num != 0L) ? num : num2); string text = ((num != 0L) ? zdo.GetString("FTL_OwnerName", string.Empty) : zdo.GetString("TGW_TamerName", string.Empty)); string text2 = zdo.GetString("FTL_Clan", string.Empty); if (string.IsNullOrWhiteSpace(text2)) { text2 = zdo.GetString("TGW_GuildName", string.Empty); } int num3 = zdo.GetInt("FTL_State", -987654321); bool flag = num3 >= 0 && num3 <= 3; TameRuntimeState state = (flag ? ((TameRuntimeState)num3) : DeriveLegacyState(zdo)); if (IsTameGuardMetadata(zdo)) { state = TameRuntimeState.TameGuard; } identity = new TameIdentity { Key = GetStableZdoKey(zdo), OwnerId = ownerId, OwnerName = (text ?? string.Empty), ClanName = NormalizeClan(text2), PrefabName = "unknown", Source = ownershipSource, State = state, HasExplicitState = flag, Zdo = zdo }; return !string.IsNullOrEmpty(identity.Key); } private static TameRuntimeState DeriveLegacyState(ZDO zdo) { if (IsTameGuardMetadata(zdo)) { return TameRuntimeState.TameGuard; } switch (zdo.GetInt("TGW_Stance", -1)) { case 0: return TameRuntimeState.Roster; case 3: case 4: return TameRuntimeState.Active; default: return TameRuntimeState.OwnedInactive; } } internal static TameRuntimeState DeriveExternalState(in TameIdentity identity) { ZDO zdo = identity.Zdo; if (zdo == null) { return identity.State; } if (IsTameGuardMetadata(zdo)) { return TameRuntimeState.TameGuard; } int num = zdo.GetInt("TGW_Stance", -1); if (identity.State == TameRuntimeState.OwnedInactive) { return identity.State; } switch (num) { case 3: case 4: return TameRuntimeState.Active; case 0: return TameRuntimeState.Roster; case 1: case 5: return TameRuntimeState.OwnedInactive; default: return identity.State; } } internal static bool IsTameGuardMetadata(ZDO zdo) { if (zdo == null) { return false; } int num = zdo.GetInt("Fehu_TCW_PatrolMode", 0); if (num == 1 || num == 2) { return true; } if (FehuTameLimitPlugin.Settings != null && FehuTameLimitPlugin.Settings.GuardWardCountsAsTameGuard.Value) { return zdo.GetInt("TGW_Stance", -1) == 2; } return false; } internal static void WriteFtl(ZDO zdo, long ownerId, string ownerName, string clan, string source, TameRuntimeState state) { if (zdo != null && ownerId != 0L) { zdo.Set("FTL_OwnerID", ownerId); zdo.Set("FTL_OwnerName", ownerName ?? string.Empty); zdo.Set("FTL_Clan", NormalizeClan(clan)); zdo.Set("FTL_Source", source ?? "FTL"); WriteState(zdo, state); } } internal static void WriteOwnershipOnly(ZDO zdo, long ownerId, string ownerName, string clan, string source) { if (zdo != null && ownerId != 0L) { zdo.Set("FTL_OwnerID", ownerId); zdo.Set("FTL_OwnerName", ownerName ?? string.Empty); zdo.Set("FTL_Clan", NormalizeClan(clan)); zdo.Set("FTL_Source", source ?? "FTL"); } } internal static void WriteState(ZDO zdo, TameRuntimeState state) { if (zdo != null) { zdo.Set("FTL_State", (int)state); zdo.Set("FTL_Active", (state == TameRuntimeState.Active) ? 1 : 0); } } internal static void EnsureExplicitState(ZDO zdo) { if (zdo != null) { int num = zdo.GetInt("FTL_State", -987654321); if (num < 0 || num > 3) { WriteState(zdo, DeriveLegacyState(zdo)); } } } internal static void UpdateClan(ZDO zdo, string clan) { if (zdo != null) { zdo.Set("FTL_Clan", NormalizeClan(clan)); } } internal static string GetStableZdoKey(ZDO zdo) { try { return ZdoUidField?.GetValue(zdo)?.ToString() ?? string.Empty; } catch { return string.Empty; } } internal static bool IsTameCandidate(GameObject go) { if ((Object)(object)go != (Object)null && (Object)(object)go.GetComponent<Tameable>() != (Object)null) { return (Object)(object)go.GetComponent<Character>() != (Object)null; } return false; } internal static bool IsActuallyTamed(GameObject go) { Character val = (((Object)(object)go != (Object)null) ? go.GetComponent<Character>() : null); if ((Object)(object)val == (Object)null) { return false; } try { MethodInfo methodInfo = AccessTools.Method(((object)val).GetType(), "IsTamed", (Type[])null, (Type[])null) ?? AccessTools.Method(typeof(Character), "IsTamed", (Type[])null, (Type[])null); return methodInfo != null && methodInfo.ReturnType == typeof(bool) && (bool)methodInfo.Invoke(val, null); } catch { return false; } } internal static string NormalizeClan(string clan) { if (string.IsNullOrWhiteSpace(clan) || string.Equals(clan, "No Guild", StringComparison.OrdinalIgnoreCase) || string.Equals(clan, "None", StringComparison.OrdinalIgnoreCase) || clan == "0") { return string.Empty; } return clan.Trim(); } internal static string NormalizePrefabName(string name) { if (string.IsNullOrWhiteSpace(name)) { return "unknown"; } if (!name.EndsWith("(Clone)", StringComparison.Ordinal)) { return name.Trim(); } return name.Substring(0, name.Length - "(Clone)".Length).Trim(); } } [DataContract] internal sealed class RegistryFile { [DataMember(Order = 1)] public int DataVersion = 3; [DataMember(Order = 2)] public long WorldUid; [DataMember(Order = 3)] public string WorldName = string.Empty; [DataMember(Order = 4)] public List<TameRecord> Tames = new List<TameRecord>(); } [DataContract] internal sealed class TameRecord { [DataMember(Order = 1)] public string Key = string.Empty; [DataMember(Order = 2)] public long OwnerId; [DataMember(Order = 3)] public string OwnerName = string.Empty; [DataMember(Order = 4)] public string ClanName = string.Empty; [DataMember(Order = 5)] public string PrefabName = "unknown"; [DataMember(Order = 6)] public int State; [DataMember(Order = 7)] public string Source = string.Empty; [DataMember(Order = 8)] public long FirstSeenUtcTicks; [DataMember(Order = 9)] public long LastSeenUtcTicks; } internal static class TameRegistry { private sealed class ReferenceEqualityComparer : IEqualityComparer<object> { internal static readonly ReferenceEqualityComparer Instance = new ReferenceEqualityComparer(); public new bool Equals(object a, object b) { return a == b; } public int GetHashCode(object obj) { return RuntimeHelpers.GetHashCode(obj); } } private static readonly Dictionary<string, TameRecord> Records = new Dictionary<string, TameRecord>(StringComparer.Ordinal); private static readonly object Gate = new object(); private static RegistryFile _file = new RegistryFile(); private static string _filePath; private static bool _dirty; private static float _nextFlush; private static int _revision; internal static int RecordCount { get { lock (Gate) { return Records.Count; } } } internal static int Revision { get { lock (Gate) { return _revision; } } } internal static void LoadWorld(long uid, string worldName) { //IL_00a4: Unknown result type (might be due to invalid IL or missing references) lock (Gate) { Records.Clear(); _filePath = Path.Combine(Paths.ConfigPath, "FehuTameLimit." + SanitizeFileName(worldName) + "-" + uid + ".json"); _file = new RegistryFile { WorldUid = uid, WorldName = (worldName ?? "world") }; if (File.Exists(_filePath)) { try { using FileStream fileStream = File.OpenRead(_filePath); if (((XmlObjectSerializer)new DataContractJsonSerializer(typeof(RegistryFile))).ReadObject((Stream)fileStream) is RegistryFile registryFile && registryFile.WorldUid == uid) { _file = registryFile; _file.DataVersion = 3; foreach (TameRecord item in registryFile.Tames ?? new List<TameRecord>()) { if (item != null && !string.IsNullOrEmpty(item.Key)) { if (item.State < 0 || item.State > 3) { item.State = 0; } Records[item.Key] = item; } } } } catch (Exception ex) { FehuTameLimitPlugin.Log.LogWarning((object)("Could not load tame registry; starting from bootstrap: " + ex.Message)); } } _dirty = false; _revision++; FehuTameLimitPlugin.Log.LogInfo((object)$"Tame registry loaded for {worldName} ({uid}): {Records.Count} records."); } } internal static int CountPlayerRoster(long ownerId, string excludeKey = null) { if (ownerId == 0L) { return 0; } lock (Gate) { return Records.Values.Count((TameRecord r) => r.OwnerId == ownerId && r.Key != excludeKey && (r.State == 1 || r.State == 2)); } } internal static int CountPlayerActive(long ownerId, string excludeKey = null) { if (ownerId == 0L) { return 0; } lock (Gate) { return Records.Values.Count((TameRecord r) => r.OwnerId == ownerId && r.Key != excludeKey && r.State == 2); } } internal static int CountClanActive(string clan, string excludeKey = null) { clan = TameOwnership.NormalizeClan(clan); if (string.IsNullOrEmpty(clan)) { return 0; } lock (Gate) { return Records.Values.Count((TameRecord r) => r.Key != excludeKey && r.State == 2 && string.Equals(TameOwnership.NormalizeClan(r.ClanName), clan, StringComparison.OrdinalIgnoreCase)); } } internal static Dictionary<long, int> GetPlayerRosterCounts() { lock (Gate) { return (from r in Records.Values where r.OwnerId != 0L && (r.State == 1 || r.State == 2) group r by r.OwnerId).ToDictionary((IGrouping<long, TameRecord> g) => g.Key, (IGrouping<long, TameRecord> g) => g.Count()); } } internal static Dictionary<long, int> GetPlayerActiveCounts() { lock (Gate) { return (from r in Records.Values where r.OwnerId != 0L && r.State == 2 group r by r.OwnerId).ToDictionary((IGrouping<long, TameRecord> g) => g.Key, (IGrouping<long, TameRecord> g) => g.Count()); } } internal static Dictionary<string, int> GetClanActiveCounts() { lock (Gate) { return Records.Values.Where((TameRecord r) => r.State == 2 && LimitService.IsRealClan(r.ClanName)).GroupBy<TameRecord, string>((TameRecord r) => TameOwnership.NormalizeClan(r.ClanName), StringComparer.OrdinalIgnoreCase).ToDictionary<IGrouping<string, TameRecord>, string, int>((IGrouping<string, TameRecord> g) => g.Key, (IGrouping<string, TameRecord> g) => g.Count(), StringComparer.OrdinalIgnoreCase); } } internal static void Upsert(TameIdentity identity) { if (string.IsNullOrEmpty(identity.Key) || identity.OwnerId == 0L) { return; } lock (Gate) { long ticks = DateTime.UtcNow.Ticks; TameRecord value; bool num = !Records.TryGetValue(identity.Key, out value); if (num) { value = new TameRecord { Key = identity.Key, FirstSeenUtcTicks = ticks }; Records[identity.Key] = value; } string text = identity.OwnerName ?? string.Empty; string text2 = TameOwnership.NormalizeClan(identity.ClanName); string text3 = identity.PrefabName ?? "unknown"; string text4 = identity.Source.ToString(); bool flag = num || value.OwnerId != identity.OwnerId || value.OwnerName != text || value.ClanName != text2 || value.PrefabName != text3 || value.State != (int)identity.State || value.Source != text4; if (flag || ticks - value.LastSeenUtcTicks >= TimeSpan.FromMinutes(5.0).Ticks) { value.OwnerId = identity.OwnerId; value.OwnerName = text; value.ClanName = text2; value.PrefabName = text3; value.State = (int)identity.State; value.Source = text4; value.LastSeenUtcTicks = ticks; _dirty = true; if (flag) { _revision++; } } } } internal static void Remove(string key) { if (string.IsNullOrEmpty(key)) { return; } lock (Gate) { if (Records.Remove(key)) { _dirty = true; _revision++; } } } internal static void Remove(GameObject go) { if (!((Object)(object)go == (Object)null)) { ZNetView component = go.GetComponent<ZNetView>(); if (!((Object)(object)component == (Object)null) && component.IsValid()) { Remove(TameOwnership.GetStableZdoKey(component.GetZDO())); } } } internal static void ReconcileLoadedCharacters() { if (!FehuTameLimitPlugin.IsServer()) { return; } int num = 0; Character[] array = Object.FindObjectsByType<Character>((FindObjectsSortMode)0); foreach (Character val in array) { if ((Object)(object)val == (Object)null) { continue; } GameObject gameObject = ((Component)val).gameObject; if (TameOwnership.TryRead(gameObject, out var _)) { TameStateService.ReconcileExternal(gameObject); num++; continue; } ZNetView component = gameObject.GetComponent<ZNetView>(); if ((Object)(object)component != (Object)null && component.IsValid()) { string stableZdoKey = TameOwnership.GetStableZdoKey(component.GetZDO()); if (!string.IsNullOrEmpty(stableZdoKey) && Contains(stableZdoKey)) { Remove(stableZdoKey); } } } if (FehuTameLimitPlugin.Settings.DebugLogging.Value) { FehuTameLimitPlugin.Log.LogDebug((object)$"Reconciled {num} owned tame records from loaded Characters."); } } internal static void RefreshClanAssignments() { if (!FehuTameLimitPlugin.IsServer()) { return; } List<TameRecord> list; lock (Gate) { list = Records.Values.Select(Clone).ToList(); } Dictionary<string, ZDO> dictionary = null; foreach (TameRecord item in list) { if (item.OwnerId == 0L || !ClanBridge.TryResolve(item.OwnerId, item.OwnerName, out var clan)) { continue; } clan = TameOwnership.NormalizeClan(clan); if (string.Equals(clan, TameOwnership.NormalizeClan(item.ClanName), StringComparison.OrdinalIgnoreCase)) { continue; } lock (Gate) { if (Records.TryGetValue(item.Key, out var value)) { value.ClanName = clan; _dirty = true; _revision++; } } if (dictionary == null) { dictionary = GetLoadedZdos(); } if (dictionary.TryGetValue(item.Key, out var value2)) { TameOwnership.UpdateClan(value2, clan); } } FlushIfDirty(); } private static Dictionary<string, ZDO> GetLoadedZdos() { Dictionary<string, ZDO> dictionary = new Dictionary<string, ZDO>(StringComparer.Ordinal); Character[] array = Object.FindObjectsByType<Character>((FindObjectsSortMode)0); foreach (Character val in array) { if ((Object)(object)val == (Object)null) { continue; } ZNetView component = ((Component)val).GetComponent<ZNetView>(); if (!((Object)(object)component == (Object)null) && component.IsValid()) { ZDO zDO = component.GetZDO(); string stableZdoKey = TameOwnership.GetStableZdoKey(zDO); if (!string.IsNullOrEmpty(stableZdoKey)) { dictionary[stableZdoKey] = zDO; } } } return dictionary; } internal static IEnumerator BootstrapWorldZdos() { if (!FehuTameLimitPlugin.IsServer() || ZDOMan.instance == null) { yield break; } FehuTameLimitPlugin.Log.LogInfo((object)"Starting one-time FTL/TGW state ZDO bootstrap."); List<ZDO> list = DiscoverZdos(ZDOMan.instance); int relevant = 0; int processed = 0; foreach (ZDO item in list) { processed++; if (TameOwnership.TryRead(item, out var identity)) { if (!identity.HasExplicitState) { TameOwnership.EnsureExplicitState(item); TameOwnership.TryRead(item, out identity); } Upsert(identity); relevant++; } if (processed % 500 == 0) { yield return null; } } ReconcileLoadedCharacters(); RefreshClanAssignments(); Flush(force: true); FehuTameLimitPlugin.Log.LogInfo((object)$"Bootstrap complete: inspected {processed} ZDOs, found {relevant} owned tame records, registry={RecordCount}."); } private static List<ZDO> DiscoverZdos(ZDOMan manager) { Dictionary<string, ZDO> dictionary = new Dictionary<string, ZDO>(StringComparer.Ordinal); HashSet<object> visited = new HashSet<object>(ReferenceEqualityComparer.Instance); FieldInfo[] fields = typeof(ZDOMan).GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (FieldInfo fieldInfo in fields) { string text = fieldInfo.Name ?? string.Empty; if (text.IndexOf("zdo", StringComparison.OrdinalIgnoreCase) >= 0 || text.IndexOf("sector", StringComparison.OrdinalIgnoreCase) >= 0 || text.IndexOf("object", StringComparison.OrdinalIgnoreCase) >= 0) { try { CollectZdos(fieldInfo.GetValue(manager), dictionary, visited, 0); } catch { } } } return dictionary.Values.ToList(); } private static void CollectZdos(object value, Dictionary<string, ZDO> found, HashSet<object> visited, int depth) { if (value == null || depth > 5) { return; } ZDO val = (ZDO)((value is ZDO) ? value : null); if (val != null) { string stableZdoKey = TameOwnership.GetStableZdoKey(val); if (!string.IsNullOrEmpty(stableZdoKey)) { found[stableZdoKey] = val; } return; } Type type = value.GetType(); if (type.IsPrimitive || value is string || value is Object || (!type.IsValueType && !visited.Add(value))) { return; } if (value is IDictionary dictionary) { { foreach (DictionaryEntry item in dictionary) { CollectZdos(item.Key, found, visited, depth + 1); CollectZdos(item.Value, found, visited, depth + 1); } return; } } if (value is IEnumerable enumerable) { int num = 0; { foreach (object item2 in enumerable) { CollectZdos(item2, found, visited, depth + 1); if (++num > 200000) { break; } } return; } } PropertyInfo property = type.GetProperty("Value", BindingFlags.Instance | BindingFlags.Public); if (!(property != null) || property.GetIndexParameters().Length != 0) { return; } try { CollectZdos(property.GetValue(value, null), found, visited, depth + 1); } catch { } } internal static void FlushIfDirty() { if (_dirty && Time.realtimeSinceStartup >= _nextFlush) { Flush(force: false); } } internal static void Flush(bool force) { //IL_009a: Unknown result type (might be due to invalid IL or missing references) lock (Gate) { if ((!_dirty && !force) || string.IsNullOrEmpty(_filePath)) { return; } try { _file.DataVersion = 3; _file.Tames = Records.Values.OrderBy<TameRecord, string>((TameRecord r) => r.Key, StringComparer.Ordinal).ToList(); string text = _filePath + ".tmp"; using (FileStream fileStream = File.Create(text)) { ((XmlObjectSerializer)new DataContractJsonSerializer(typeof(RegistryFile))).WriteObject((Stream)fileStream, (object)_file); fileStream.Flush(flushToDisk: true); } if (File.Exists(_filePath)) { File.Copy(text, _filePath, overwrite: true); File.Delete(text); } else { File.Move(text, _filePath); } _dirty = false; _nextFlush = Time.realtimeSinceStartup + 5f; } catch (Exception ex) { FehuTameLimitPlugin.Log.LogWarning((object)("Could not save tame registry: " + ex.Message)); } } } private static bool Contains(string key) { lock (Gate) { return Records.ContainsKey(key); } } private static TameRecord Clone(TameRecord r) { return new TameRecord { Key = r.Key, OwnerId = r.OwnerId, OwnerName = r.OwnerName, ClanName = r.ClanName, PrefabName = r.PrefabName, State = r.State, Source = r.Source, FirstSeenUtcTicks = r.FirstSeenUtcTicks, LastSeenUtcTicks = r.LastSeenUtcTicks }; } private static string SanitizeFileName(string value) { char[] invalidFileNameChars = Path.GetInvalidFileNameChars(); foreach (char oldChar in invalidFileNameChars) { value = (value ?? "world").Replace(oldChar, '_'); } if (!string.IsNullOrWhiteSpace(value)) { return value; } return "world"; } } internal static class TameStateService { internal static bool TryTransition(GameObject go, TameRuntimeState target, Player actor, bool enforce, out LimitDecision decision) { decision = default(LimitDecision); if ((Object)(object)go == (Object)null || !TameOwnership.TryRead(go, out var identity) || !identity.HasOwner) { return false; } string text = OwnershipService.ResolveClan(identity.OwnerId, identity.OwnerName, identity.ClanName); if (!string.Equals(text, identity.ClanName, StringComparison.OrdinalIgnoreCase)) { identity.ClanName = text; TameOwnership.UpdateClan(identity.Zdo, text); } decision = LimitService.CanTransition(in identity, target); if (enforce && !decision.Allowed) { if ((Object)(object)actor != (Object)null) { TgwIntegration.Show(actor, decision.Message); } else { NetworkSync.SendNotice(identity.OwnerId, decision.Message); } return false; } TameOwnership.WriteState(identity.Zdo, target); identity.State = target; identity.HasExplicitState = true; TameRegistry.Upsert(identity); TameRegistry.FlushIfDirty(); if (FehuTameLimitPlugin.Settings.DebugLogging.Value) { FehuTameLimitPlugin.Log.LogDebug((object)$"State {identity.PrefabName} [{identity.Key}] => {target}; owner={identity.OwnerId}, clan='{identity.ClanName}'."); } return true; } internal static TameRuntimeState ChooseInitialState(GameObject go, long ownerId, string ownerName, string clan, bool preferRoster) { if ((Object)(object)go == (Object)null) { return TameRuntimeState.OwnedInactive; } ZNetView component = go.GetComponent<ZNetView>(); if ((Object)(object)component != (Object)null && component.IsValid() && TameOwnership.IsTameGuardMetadata(component.GetZDO())) { return TameRuntimeState.TameGuard; } if (!preferRoster || !FehuTameLimitPlugin.Settings.NewTamesEnterRoster.Value) { return TameRuntimeState.OwnedInactive; } if (!LimitService.CanTransition(new TameIdentity { Key = (((Object)(object)component != (Object)null && component.IsValid()) ? TameOwnership.GetStableZdoKey(component.GetZDO()) : string.Empty), OwnerId = ownerId, OwnerName = (ownerName ?? string.Empty), ClanName = TameOwnership.NormalizeClan(clan), PrefabName = TameOwnership.NormalizePrefabName(((Object)go).name), Source = OwnershipSource.FehuTameLimit, State = TameRuntimeState.OwnedInactive, Zdo = (((Object)(object)component != (Object)null && component.IsValid()) ? component.GetZDO() : null), GameObject = go }, TameRuntimeState.Roster).Allowed) { return TameRuntimeState.OwnedInactive; } return TameRuntimeState.Roster; } internal static void ReconcileExternal(GameObject go) { if ((Object)(object)go == (Object)null || !TameOwnership.TryRead(go, out var identity)) { return; } TameRuntimeState tameRuntimeState = TameOwnership.DeriveExternalState(in identity); if (!identity.HasExplicitState) { TameOwnership.WriteState(identity.Zdo, tameRuntimeState); identity.State = tameRuntimeState; identity.HasExplicitState = true; TameRegistry.Upsert(identity); return; } if (tameRuntimeState == identity.State) { TameRegistry.Upsert(identity); return; } TameOwnership.WriteState(identity.Zdo, tameRuntimeState); identity.State = tameRuntimeState; TameRegistry.Upsert(identity); if (FehuTameLimitPlugin.Settings.DebugLogging.Value) { FehuTameLimitPlugin.Log.LogDebug((object)$"Reconciled external state [{identity.Key}] => {tameRuntimeState}."); } } } internal static class TgwIntegration { internal enum IntegrationState { UNINITIALIZED, READY, FAILED_SAFE } private sealed class StanceState { internal GameObject Go; internal TameRuntimeState Target; internal bool Commit; } private const string TypeName = "TameGuildWars.TamedGuildMob"; private const string PluginGuid = "WackyMole.TameGuildWars"; private static IntegrationState _state; private static Type _tgwType; internal static bool IsReady => _state == IntegrationState.READY; internal static IntegrationState State => _state; internal static void InitializeOnce(Harmony harmony) { //IL_0107: 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_0129: Expected O, but got Unknown //IL_0129: Expected O, but got Unknown if (_state != IntegrationState.UNINITIALIZED) { return; } if (harmony == null) { FailSafe(); return; } try { if (!Chainloader.PluginInfos.ContainsKey("WackyMole.TameGuildWars")) { FailSafe(); return; } _tgwType = FindLoadedType("TameGuildWars.TamedGuildMob"); if (_tgwType == null) { FailSafe(); return; } MethodInfo[] array = (from m in _tgwType.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) where m.Name == "SetStance" select m).ToArray(); if (array.Length == 0) { FailSafe(); return; } PatchPostfix(harmony, AccessTools.Method(_tgwType, "TryClaim", (Type[])null, (Type[])null), "PostfixMutation"); PatchPostfix(harmony, AccessTools.Method(_tgwType, "TryChangeTamer", (Type[])null, (Type[])null), "PostfixMutation"); PatchPostfix(harmony, AccessTools.Method(_tgwType, "TryChangeGuild", (Type[])null, (Type[])null), "PostfixMutation"); MethodInfo[] array2 = array; foreach (MethodInfo methodInfo in array2) { harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(TgwIntegration), "PrefixSetStance", (Type[])null), new HarmonyMethod(typeof(TgwIntegration), "PostfixSetStance", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } _state = IntegrationState.READY; FehuTameLimitPlugin.Log.LogInfo((object)"[FehuTameLimit] TGW integration: READY"); } catch { FailSafe(); } } private static void FailSafe() { if (_state == IntegrationState.UNINITIALIZED) { _state = IntegrationState.FAILED_SAFE; FehuTameLimitPlugin.Log.LogWarning((object)"[FehuTameLimit] TGW integration: FAILED-SAFE"); } } private static Type FindLoadedType(string fullName) { Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { try { Type type = assembly.GetType(fullName, throwOnError: false); if (type != null) { return type; } } catch { } } return null; } private static void PatchPostfix(Harmony harmony, MethodInfo original, string postfix) { //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Expected O, but got Unknown if (!(original == null)) { harmony.Patch((MethodBase)original, (HarmonyMethod)null, new HarmonyMethod(typeof(TgwIntegration), postfix, (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } } private static bool PrefixSetStance(object __instance, object[] __args, ref bool __result, ref object __state) { __state = null; if (!FehuTameLimitPlugin.IsServer() || !FehuTameLimitPlugin.Settings.Enabled.Value) { return true; } object obj = ((__instance is Component) ? __instance : null); GameObject val = ((obj != null) ? ((Component)obj).gameObject : null); if ((Object)(object)val == (Object)null) { return true; } if (!TryGetStance(__args, out var stance)) { return true; } TameRuntimeState tameRuntimeState = StateFromStance(stance); Player val2 = FindPlayer(__args); if (!TameOwnership.TryRead(val, out var identity)) { if ((Object)(object)val2 != (Object)null) { OwnershipService.TryAssign(val, val2, "TGW-stance", preferRoster: false); } if (!TameOwnership.TryRead(val, out identity)) { return true; } } string clan = (identity.ClanName = OwnershipService.ResolveClan(identity.OwnerId, identity.OwnerName, identity.ClanName)); if (identity.Zdo != null) { TameOwnership.UpdateClan(identity.Zdo, clan); if (identity.Zdo.GetLong("FTL_OwnerID", 0L) == 0L) { TameOwnership.WriteOwnershipOnly(identity.Zdo, identity.OwnerId, identity.OwnerName, clan, "TGW"); } } if (tameRuntimeState != TameRuntimeState.TameGuard) { LimitDecision limitDecision = LimitService.CanTransition(in identity, tameRuntimeState); if (!limitDecision.Allowed) { if ((Object)(object)val2 != (Object)null) { Show(val2, limitDecision.Message); } else { NetworkSync.SendNotice(identity.OwnerId, limitDecision.Message); } __result = false; return false; } } __state = new StanceState { Go = val, Target = tameRuntimeState, Commit = true }; return true; } private static void PostfixSetStance(bool __result, object __state) { if (__result && __state is StanceState { Commit: not false } stanceState && !((Object)(object)stanceState.Go == (Object)null)) { TameStateService.TryTransition(stanceState.Go, stanceState.Target, null, enforce: false, out var _); } } private static void PostfixMutation(object __instance, bool __result) { if (!FehuTameLimitPlugin.IsServer() || !__result) { return; } object obj = ((__instance is Component) ? __instance : null); GameObject val = ((obj != null) ? ((Component)obj).gameObject : null); if ((Object)(object)val == (Object)null || !TameOwnership.TryRead(val, out var identity)) { return; } string clan = OwnershipService.ResolveClan(identity.OwnerId, identity.OwnerName, identity.ClanName); if (identity.Zdo == null || identity.Zdo.GetLong("FTL_OwnerID", 0L) != identity.OwnerId) { OwnershipService.TryAssign(val, identity.OwnerId, identity.OwnerName, clan, "TGW", preferRoster: true); } else { TameOwnership.WriteOwnershipOnly(identity.Zdo, identity.OwnerId, identity.OwnerName, clan, "TGW"); if (!identity.HasExplicitState) { TameOwnership.WriteState(identity.Zdo, TameStateService.ChooseInitialState(val, identity.OwnerId, identity.OwnerName, clan, preferRoster: true)); } if (TameOwnership.TryRead(val, out var identity2)) { TameRegistry.Upsert(identity2); } } TameRegistry.FlushIfDirty(); } private static bool TryGetStance(object[] args, out int stance) { stance = -1; if (args == null) { return false; } foreach (object obj in args) { if (obj == null) { continue; } Type type = obj.GetType(); if (type.IsEnum && (type.FullName ?? string.Empty).IndexOf("GuildMobStance", StringComparison.OrdinalIgnoreCase) >= 0) { try { stance = Convert.ToInt32(obj); return true; } catch { return false; } } } return false; } private static Player FindPlayer(object[] args) { if (args != null) { foreach (object obj in args) { Player val = (Player)((obj is Player) ? obj : null); if (val != null) { return val; } } } return null; } private static TameRuntimeState StateFromStance(int stance) { switch (stance) { case 2: return TameRuntimeState.TameGuard; case 3: case 4: return TameRuntimeState.Active; case 0: return TameRuntimeState.Roster; case 1: case 5: return TameRuntimeState.OwnedInactive; default: return TameRuntimeState.OwnedInactive; } } internal static void Show(Player player, string message) { try { if ((Object)(object)player != (Object)null) { ((Character)player).Message((MessageType)2, message, 0, (Sprite)null, false); } } catch { } } } internal static class VanillaIntegration { private static bool _installed; internal static void TryInstall(Harmony harmony) { //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Expected O, but got Unknown //IL_009b: Unknown result type (might be due to invalid IL or missing references) //IL_00a8: Expected O, but got Unknown if (_installed || harmony == null) { return; } try { Type? typeFromHandle = typeof(Tameable); MethodInfo methodInfo = AccessTools.Method(typeFromHandle, "Tame", (Type[])null, (Type[])null); if (methodInfo != null) { harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(typeof(VanillaIntegration), "PostfixTame", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } foreach (MethodInfo item in from m in typeFromHandle.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) where m.Name == "Interact" select m) { harmony.Patch((MethodBase)item, (HarmonyMethod)null, new HarmonyMethod(typeof(VanillaIntegration), "PostfixInteract", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } _installed = true; FehuTameLimitPlugin.Log.LogInfo((object)"Vanilla Tameable ownership integration installed (ownership unlimited)."); } catch (Exception arg) { FehuTameLimitPlugin.Log.LogError((object)$"Vanilla Tameable integration failed: {arg}"); } } private static void PostfixTame(object __instance) { //IL_0046: Unknown result type (might be due to invalid IL or missing references) if (!FehuTameLimitPlugin.IsServer() || !FehuTameLimitPlugin.Settings.Enabled.Value) { return; } object obj = ((__instance is Component) ? __instance : null); GameObject val = ((obj != null) ? ((Component)obj).gameObject : null); if (!((Object)(object)val == (Object)null) && !TameOwnership.TryRead(val, out var _)) { Player val2 = OwnershipService.FindNearestPlayer(val.transform.position, FehuConfig.PositiveOrDefault(FehuTameLimitPlugin.Settings.OwnershipAssignRadius.Value, 1f, 100f)); if (!((Object)(object)val2 == (Object)null)) { OwnershipService.TryAssign(val, val2, "vanilla-tame", preferRoster: true); TameRegistry.FlushIfDirty(); } } } private static void PostfixInteract(object __instance, object[] __args) { if (!FehuTameLimitPlugin.IsServer() || !FehuTameLimitPlugin.Settings.AdoptOnTamedInteract.Value) { return; } object obj = ((__instance is Component) ? __instance : null); GameObject val = ((obj != null) ? ((Component)obj).gameObject : null); if ((Object)(object)val == (Object)null || TameOwnership.TryRead(val, out var _) || !TameOwnership.IsActuallyTamed(val)) { return; } Player val2 = null; if (__args != null) { foreach (object obj2 in __args) { Player val3 = (Player)((obj2 is Player) ? obj2 : null); if (val3 != null) { val2 = val3; break; } } } if (!((Object)(object)val2 == (Object)null)) { OwnershipService.TryAssign(val, val2, "legacy-interact", preferRoster: true); TameRegistry.FlushIfDirty(); } } }